Compare commits

...

656 Commits

Author SHA1 Message Date
Mononaut
5682b157c1 support for acceleration mempool blocks animation 2023-06-13 13:35:25 -04:00
Mononaut
77b0a8eccc Refactor accelerated audits 2023-06-07 16:47:54 -04:00
Mononaut
6af6274f43 Implement accelerations API & config setting 2023-06-07 16:47:54 -04:00
Mononaut
379fa3fe49 Refactor acceleration tracking 2023-06-07 16:47:54 -04:00
Mononaut
fe76a0fd08 use accelerated rates for block templates & show in viz 2023-06-07 16:47:54 -04:00
Mononaut
412d83f961 include accelerated tx data in block audits 2023-06-07 16:47:52 -04:00
softsimon
9a99ee6486 Merge pull request #3772 from mempool/nymkappa/dont-rethrow-block-summaries
[audit] warn if we cannot save templates and remove exception re-throw
2023-06-07 18:31:59 +02:00
nymkappa
fd30bff9c6 don't throw when BlocksAuditRepositories.$saveAudit fails 2023-06-07 18:04:21 +02:00
softsimon
804640216f Merge pull request #3830 from mempool/fix/p2tr-annex-parse
Fix: Annex parsing for p2tr on bitcoind/romanz backends
2023-06-06 20:27:03 +04:00
softsimon
2191bf2a22 Merge pull request #3826 from mempool/mononaut/fix-firstseen
fix firstSeen reset migration bug
2023-06-06 20:18:35 +04:00
softsimon
7d92cdf015 Merge pull request #3811 from mempool/mononaut/fix-median-fee-bug
Fix missing fees in $updateBlocks without esplora
2023-06-06 19:35:45 +04:00
Mononaut
386037d1db Fix missing fees in $updateBlocks without esplora 2023-06-06 17:34:22 +02:00
softsimon
f2e216b9ac Merge pull request #3804 from mempool/mononaut/split-summaries-table
Break block templates into their own db table
2023-06-06 19:30:28 +04:00
softsimon
eb8c38e4e8 Merge pull request #3832 from mempool/mononaut/fix-graph-filter-colors
fix graph filter dropdown colors
2023-06-06 17:53:56 +04:00
Mononaut
689319437a fix graph filter dropdown colors 2023-06-05 14:23:37 -04:00
junderw
9e1de656c1 Fix: Annex parsing for p2tr on bitcoind/romanz backends 2023-06-05 07:21:55 -07:00
Mononaut
37dd95a4a0 fix firstSeen reset bug 2023-06-04 12:47:04 -04:00
softsimon
6076eeed46 Merge pull request #3825 from mempool/mononaut/fix-negative-confs
Fix negative confirmations
2023-06-04 20:23:35 +04:00
Mononaut
8b1dff6d15 fix txConfirmed type 2023-06-04 11:47:46 -04:00
softsimon
9d4b58604b Optimize change detection 2023-06-04 10:32:24 +04:00
Mononaut
c49626aefc Confirmation badge component, fix negative confirmations 2023-06-03 16:20:32 -04:00
softsimon
baeb200e8b Merge pull request #3815 from mempool/hunicus/unmute-promo
Unmute about promo video on click
2023-06-03 15:35:31 +04:00
softsimon
11669849ab Merge pull request #3818 from mempool/mononaut/calculate-sigops
Count sigops and use adjusted vsizes
2023-06-01 09:49:54 +07:00
Mononaut
b171ed6dd0 Break block templates into their own db table 2023-05-31 13:43:48 -04:00
Mononaut
0ac76e12c4 Fix frontend logic for displaying effective fee rate 2023-05-31 12:11:56 -04:00
Mononaut
ee1ec414ed use fractional base vsize in adjusted vsize 2023-05-31 11:45:47 -04:00
Mononaut
0e5dc21854 Fix mined rbf / calculate sigop merge conflicts 2023-05-31 11:37:13 -04:00
Mononaut
bf7df08305 Enforce block sigop limits in GBT algorithm 2023-05-31 11:29:58 -04:00
Mononaut
ec63c822db Display sigops & adjusted vsize in transaction page details 2023-05-31 11:29:58 -04:00
Mononaut
09e4e44e88 Count sigops & use adjusted vsizes in mempool projections 2023-05-31 11:29:56 -04:00
Mononaut
c5e6821ad4 show fee rating badge either on rate or effective rate 2023-05-31 11:27:04 -04:00
Mononaut
70fa78b987 Fix effective fee rates for non-cpfp dependents 2023-05-31 11:27:04 -04:00
softsimon
8c78e35063 Use native angular way of accessing HTML elements 2023-05-31 14:39:19 +07:00
wiz
126a75ed45 ops: Use mempool/electrs fork of blockstream/electrs 2023-05-30 12:53:38 -03:00
softsimon
0703690190 Merge pull request #3785 from mempool/mononaut/mined-block-rbf
Detect RBF by mined transactions
2023-05-30 14:50:07 +07:00
wiz
7f56f0295b Merge pull request #3817 from mempool/ops/add-backend-reindex-macros
ops: Add npm run scripts for backend reindexing
2023-05-29 14:21:01 -03:00
wiz
27154da191 ops: Add npm run scripts for backend reindexing 2023-05-29 14:09:28 -03:00
hunicus
0fb61fded3 Add touch event for touchscreens 2023-05-27 13:20:55 -04:00
hunicus
ce00814bbf Unmute about promo video on click 2023-05-28 01:40:30 +09:00
wiz
c758a3538b Merge pull request #3806 from mempool/dependabot/npm_and_yarn/backend/bitcoinjs-lib-6.1.1 2023-05-27 12:16:29 -03:00
Felipe Knorr Kuhn
60fd2df4e6 Merge branch 'master' into dependabot/npm_and_yarn/backend/bitcoinjs-lib-6.1.1 2023-05-27 08:13:46 -07:00
wiz
05e38b43b9 Merge pull request #3809 from mempool/hunicus/tm-r
Switch tm to r for registered trademarks
2023-05-26 23:50:35 -03:00
hunicus
9fa1eebc02 Switch tm to r for registered trademarks 2023-05-27 06:46:18 +09:00
wiz
79ea1ee257 Merge pull request #3797 from mempool/hunicus/add-bullbit
Add bull bitcoin as enterprise sponsor
2023-05-26 11:34:40 -03:00
wiz
add405481e Merge pull request #3776 from knorrium/docker_vars_test
Docker vars tests
2023-05-26 11:33:48 -03:00
Felipe Knorr Kuhn
c382e03e4a Merge branch 'master' into docker_vars_test 2023-05-26 07:16:29 -07:00
dependabot[bot]
ac73de31b5 Bump bitcoinjs-lib from 6.1.0 to 6.1.1 in /backend
Bumps [bitcoinjs-lib](https://github.com/bitcoinjs/bitcoinjs-lib) from 6.1.0 to 6.1.1.
- [Changelog](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bitcoinjs/bitcoinjs-lib/compare/v6.1.0...v6.1.1)

---
updated-dependencies:
- dependency-name: bitcoinjs-lib
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-26 02:56:38 +00:00
wiz
ccd403b771 Merge branch 'master' into hunicus/add-bullbit 2023-05-25 19:18:00 -03:00
wiz
f28c460bb4 Merge pull request #3796 from mempool/hunicus/fka-embassyos
Change embassyos to startos
2023-05-25 19:17:53 -03:00
hunicus
5711c939d7 Make svg pretty 2023-05-25 15:58:48 -04:00
wiz
ef3ce6d187 Merge pull request #3799 from Emzy/ops/nginx-json-match 2023-05-25 14:58:45 -03:00
wiz
410e604dce Merge pull request #3801 from mempool/nymkappa/reindexing
Always re-indexing blocks when updating mining pools
2023-05-25 13:10:10 -03:00
nymkappa
8ffe2f3156 Always re-indexing blocks when updating mining pools 2023-05-25 09:08:51 -07:00
wiz
d27febf878 Merge pull request #3800 from mempool/simon/dont-wipe-mempool-cache
Don't wipe mempool cache in pools updater
2023-05-25 12:37:51 -03:00
softsimon
038f9659bb Save new disk cache after ignoring blocks 2023-05-25 19:19:14 +04:00
softsimon
aeb896e200 Don't wipe mempool cache in pools updater 2023-05-25 19:07:51 +04:00
softsimon
349ba613dd Revert "Don't wipe mempool cache in pools updater"
This reverts commit 714208a366.
2023-05-25 19:06:45 +04:00
softsimon
714208a366 Don't wipe mempool cache in pools updater 2023-05-25 19:05:29 +04:00
Stephan Oeste
ad4c7d7c0b Fix exact match of .js files. Was also matching .json before. 2023-05-25 16:50:56 +02:00
hunicus
264c02eca1 Add bull bitcoin as enterprise sponsor 2023-05-25 01:29:55 -04:00
hunicus
ea42077320 Change embassyos to startos 2023-05-25 00:31:44 -04:00
softsimon
92ffb441a1 Merge pull request #3791 from mempool/simon/pool-detection-addresses-fix
Fix for pool output address matching
2023-05-24 19:49:41 +04:00
wiz
725526d733 Merge pull request #3637 from mempool/ops/installer-fix-cln-folder-creation
ops: Fix installer creation of CLN folders
2023-05-23 18:34:28 -03:00
softsimon
31ea2e1d4b Fix for pool output address matching
fixes #3782
2023-05-24 01:22:01 +04:00
wiz
c81509d762 ops: Start mysql-server using onestart from install 2023-05-23 17:57:27 -03:00
wiz
07f16165ff ops: Disable buggy CLN crontab job in install 2023-05-23 17:23:51 -03:00
softsimon
536f29eb0d Merge pull request #3783 from mempool/mononaut/high-sigop-audit
Omit possible high-sigop txs from block health score
2023-05-21 19:32:26 -04:00
Mononaut
369db7a63c Detect RBF by mined transactions 2023-05-18 09:51:41 -04:00
Mononaut
81ec54fcb3 Omit possible high-sigop txs from block health score 2023-05-17 11:46:50 -04:00
softsimon
64d6bda728 Merge pull request #3780 from mempool/mononaut/fix-graph-filter
Fix mempool graph fee filtering
2023-05-16 23:37:12 -04:00
Mononaut
7ab05d815d Fix mempool graph fee filtering 2023-05-16 16:25:38 -06:00
Felipe Knorr Kuhn
50a865e54e Merge branch 'master' into docker_vars_test 2023-05-15 13:09:46 -07:00
softsimon
2156fb2a83 Merge pull request #3406 from mempool/nymkappa/block-health-history-i18n
Rename block prediction to block health
2023-05-14 16:39:43 -05:00
Felipe Knorr Kuhn
4e1087801a Merge branch 'master' into docker_vars_test 2023-05-14 14:34:47 -07:00
nymkappa
4dec152df0 Blocks Health -> Block Health 2023-05-14 16:32:11 -05:00
nymkappa
720c2b8807 Block prediction -> Block health - Fix wrong chart download name 2023-05-14 16:32:11 -05:00
Felipe Knorr Kuhn
b00e0b6a73 Fix Docker config file to use the correct types 2023-05-14 14:26:38 -07:00
Felipe Knorr Kuhn
6333f3aa47 Update the backend config fixture 2023-05-14 14:25:23 -07:00
softsimon
7b127ebe8b Merge pull request #3653 from mempool/mononaut/fix-mempool-block-skeleton
Fix mempool block skeleton loaders
2023-05-14 16:20:24 -05:00
softsimon
585309f9f0 Merge branch 'master' into mononaut/fix-mempool-block-skeleton 2023-05-14 16:18:37 -05:00
Felipe Knorr Kuhn
184ca3c662 Add a test to check that the docker json is in sync with the template 2023-05-14 14:17:53 -07:00
softsimon
bfea535df1 Merge pull request #3716 from mempool/mononaut/skeleton-fee
display fee box skeleton while mempool not in sync
2023-05-14 16:16:02 -05:00
softsimon
65189d6f3e Merge branch 'master' into mononaut/skeleton-fee 2023-05-14 16:10:39 -05:00
Felipe Knorr Kuhn
de434bb398 Add missing Lightning config 2023-05-14 13:26:35 -07:00
Felipe Knorr Kuhn
804485a526 Add missing DATABASE_TIMEOUT config 2023-05-14 13:20:18 -07:00
Felipe Knorr Kuhn
3f8fefcb0e Add a test to check if all the vars are on the Docker script 2023-05-14 13:14:44 -07:00
Felipe Knorr Kuhn
178da3df26 Use the same delimiter for all substitutions 2023-05-14 13:13:37 -07:00
softsimon
02b5fadf44 Merge pull request #3774 from mempool/mononaut/fix-clock-fee
Remove clock fee debugging adjustment
2023-05-13 12:16:32 -05:00
Mononaut
ea52d40a10 Remove clock fee debugging adjustment 2023-05-13 09:33:15 -06:00
softsimon
cbd4ea1aa8 Merge pull request #3771 from mempool/mononaut/clock-mempool-subscription
Add missing clock websocket subscriptions
2023-05-13 08:35:05 -05:00
nymkappa
f95c16a78e [audit] warn if we cannot save templates and remove exception re-throw 2023-05-13 15:28:29 +02:00
softsimon
fa1cf7abb9 Merge pull request #3768 from mempool/hunicus/tm-symbol
Add trademark symbol to trademarks in footer
2023-05-13 08:04:55 -05:00
Mononaut
6a5afd7f95 Add missing clock websocket subscriptions 2023-05-12 21:08:12 -06:00
hunicus
0ddd9b2487 Turn generic project text into trademark 2023-05-12 15:54:35 -04:00
hunicus
ee1e75f978 Add trademark symbol to trademarks in footer 2023-05-12 12:55:51 -04:00
softsimon
97c8ace8f7 Merge pull request #3749 from mempool/nymkappa/fix-3747
[mempool graph] show horizontal guide line
2023-05-12 10:54:05 -05:00
softsimon
a3ca79e995 Temporarily hide sign in 2023-05-12 10:53:42 -05:00
softsimon
962c9543d0 Merge pull request #3752 from mempool/simon/global-footer-fixes
Global footer fixes
2023-05-12 10:35:01 -05:00
softsimon
52bcef54be Only show sign in on mempool base module 2023-05-12 10:34:32 -05:00
softsimon
22030027b4 Merge pull request #3755 from mempool/mononaut/better-precise-durations
smarter time duration unit selection
2023-05-12 10:31:16 -05:00
hunicus
53a9093a70 Center left footer panel vertically 2023-05-12 11:29:17 -04:00
softsimon
6ef793f202 Merge pull request #3760 from knorrium/fix_rbf_test
Update mocks and fix RBF tests
2023-05-12 10:29:07 -05:00
softsimon
b227ff8e1b Update websocket RBF fixture 2023-05-12 10:14:21 -05:00
Felipe Knorr Kuhn
db6b9ffa6e Merge branch 'master' into fix_rbf_test 2023-05-12 07:43:54 -07:00
softsimon
5a0c809fa7 Fix linting 2023-05-12 09:37:19 -05:00
softsimon
f6caed3ffd Hide sign in on non official 2023-05-12 09:31:12 -05:00
softsimon
6141516eb2 Merge pull request #3753 from mempool/mononaut/fix-difficulty-estimate
Fix difficulty estimate
2023-05-12 09:21:06 -05:00
Felipe Knorr Kuhn
3b295b5482 Update mocks and fix RBF tests 2023-05-11 21:33:47 -07:00
softsimon
adc395fc3d Merge pull request #3743 from mempool/mononaut/full-stack-fee-stats
stack-of-n-blocks fee statistics
2023-05-11 19:48:02 -05:00
hunicus
0c52898010 Add spacing to compensate for links under button 2023-05-11 20:43:57 -04:00
hunicus
4fe6a74d20 Vary network urls according to base module 2023-05-11 20:37:11 -04:00
softsimon
7fca18d7be Merge pull request #3751 from mempool/mononaut/fix-rbf-times
Fix RBF timestamps to always use seconds
2023-05-11 19:35:29 -05:00
hunicus
208756bdd2 Show explorer links conditionally 2023-05-11 19:31:22 -04:00
hunicus
82a072bd87 Remove footer from bisq docs 2023-05-11 19:04:24 -04:00
softsimon
6d76d11837 Merge pull request #3742 from vostrnad/empty-witness-items
Display empty witness items
2023-05-11 17:51:44 -05:00
hunicus
982f1e007a Fix bisq quirks 2023-05-11 18:48:53 -04:00
hunicus
a4bf545993 Fix footer on bisq 2023-05-11 18:44:42 -04:00
softsimon
fe51f6a504 Merge pull request #3745 from mempool/mononaut/address-false-positive
Fix txids interpreted as addresses in search bar
2023-05-11 17:34:19 -05:00
softsimon
f5d311ca44 Fixes for Liquid 2023-05-11 17:14:16 -05:00
hunicus
b81dd99825 Remove social icons and version from about page 2023-05-11 17:57:04 -04:00
hunicus
f08457aba7 Improve responsiveness and add social icons 2023-05-11 17:46:38 -04:00
Mononaut
c601e5dcb4 smarter time duration unit selection 2023-05-11 13:33:38 -06:00
hunicus
11b82b3459 Remove third column on footer 2023-05-11 15:31:48 -04:00
Mononaut
d322665789 update difficulty tests 2023-05-11 11:39:18 -06:00
Mononaut
49529627f8 Fix difficulty adjustment calculation 2023-05-11 11:18:58 -06:00
softsimon
107746feec Global footer fixes 2023-05-11 11:38:57 -05:00
Mononaut
e5bef55d47 Fix RBF timestamps to always use seconds 2023-05-11 09:21:48 -06:00
wiz
17dd02ed4e Merge pull request #3736 from mempool/mononaut/optimize-websocket-updates
Optimize websocket updates
2023-05-11 09:57:08 -05:00
nymkappa
7bb34fe090 [mempool graph] show horizontal guide line 2023-05-11 14:30:57 +02:00
Mononaut
abbaee0274 Fix txids interpreted as addresses in search 2023-05-10 19:57:58 -06:00
Mononaut
3d1cd3193a online calculation of stack-of-n-blocks fee statistics 2023-05-10 17:34:20 -06:00
Vojtěch Strnad
be53cd8b48 Display empty witness items 2023-05-11 00:11:23 +02:00
Vojtěch Strnad
4b20ea7232 Accept the CLA for @vostrnad 2023-05-11 00:10:57 +02:00
Mononaut
ffd7831efc optimize websocket init data 2023-05-10 08:05:39 -06:00
Mononaut
f8636d20c2 optimize batch client websocket updates 2023-05-10 08:05:39 -06:00
wiz
3b4dd7e633 Merge pull request #3724 from mempool/hunicus/big-footer
Add big footer
2023-05-09 13:45:43 -05:00
wiz
ea101e65bb Merge branch 'master' into hunicus/big-footer 2023-05-09 13:31:58 -05:00
wiz
8a713c3880 Merge pull request #3732 from mempool/mononaut/more-fee-bands
Increase displayed fee bands
2023-05-09 13:31:50 -05:00
wiz
42d5650bc0 Merge branch 'master' into mononaut/more-fee-bands 2023-05-09 13:20:04 -05:00
wiz
ee2bc2282a Merge pull request #3731 from mempool/mononaut/clocktower
Mempool clocks
2023-05-09 12:51:01 -05:00
wiz
34565dc675 Merge branch 'master' into mononaut/clocktower 2023-05-09 12:36:58 -05:00
wiz
6e57de3220 Merge pull request #3735 from mempool/mononaut/optimize-gbt-main-thread
Optimize main thread processing of GBT updates
2023-05-10 00:13:32 +09:00
Mononaut
033e78c0a7 Optimize main thread processing of GBT updates 2023-05-08 23:01:17 -06:00
Mononaut
5257716e1a Dynamic fee ranges & legend in mempool graph 2023-05-08 12:53:37 -06:00
Mononaut
47b95af8ae increase range of fee colors 2023-05-08 12:44:14 -06:00
Mononaut
f20bfb025b fix clock merge conflicts 2023-05-08 09:11:30 -06:00
Mononaut
9671259f5c clock selected block arrow 2023-05-08 08:48:56 -06:00
Mononaut
07dddd857b resize clock labels 2023-05-08 08:48:56 -06:00
Mononaut
19353fc1d0 rename clock components 2023-05-08 08:48:56 -06:00
Mononaut
1fccd70379 clock size query params 2023-05-08 08:48:55 -06:00
Mononaut
fdb0cf509d query param toggle for clock stats 2023-05-08 08:48:55 -06:00
Mononaut
056d61a28d clock i18n 2023-05-08 08:48:55 -06:00
Mononaut
d3a7950e78 Add clock statistics 2023-05-08 08:48:55 -06:00
Mononaut
3ddd51d4cb implement clock face & dial 2023-05-08 08:48:55 -06:00
Mononaut
f879a34021 responsive clock, fix blockchain 2023-05-08 08:48:55 -06:00
Mononaut
61531171c9 clocktower top blocks & layout adjustment 2023-05-08 08:48:53 -06:00
Mononaut
3b459b6857 hourly blocks clock faces 2023-05-08 08:46:18 -06:00
softsimon
be5882edb3 Merge pull request #3710 from mempool/mononaut/optimize-gbt-data
Mononaut/optimize gbt data
2023-05-07 23:55:27 +04:00
Mononaut
428d4fc6ab optimize data structures for advanced GBT algorithm 2023-05-07 11:54:23 -06:00
softsimon
07d9315bbe Merge pull request #3727 from mempool/simon/rapid-mempool-sync
Rapid mempool sync
2023-05-07 20:22:10 +04:00
softsimon
ee05a6852e Rapid mempool sync 2023-05-07 20:21:02 +04:00
softsimon
64b3e7ad50 Merge pull request #3655 from mempool/mononaut/mempool-delete-race-condition
Fix new block mempool eviction race condition
2023-05-07 20:18:36 +04:00
hunicus
822934828a Add initial content to big footer 2023-05-06 04:10:17 -04:00
softsimon
8866923efa Merge pull request #3721 from mempool/mononaut/fix-first-seen
Restore transaction first seen field
2023-05-06 03:05:35 +04:00
Mononaut
06ef114395 Restore transaction first seen field 2023-05-05 15:12:05 -07:00
softsimon
918a3ef115 Merge pull request #3677 from mempool/mononaut/duration-precision
More precise durations in difficulty components
2023-05-06 01:58:16 +04:00
softsimon
5e9e570b99 Merge branch 'master' into mononaut/duration-precision 2023-05-06 01:39:50 +04:00
softsimon
3e6c672a1e Merge pull request #3676 from mempool/mononaut/better-durations
Improve unit selection for duration formatting
2023-05-06 01:39:37 +04:00
Mononaut
387a38a1c8 Increase precision of some formatted durations 2023-05-05 14:35:57 -07:00
Mononaut
78e86c7c55 Improve unit selection for duration formatting 2023-05-05 14:30:03 -07:00
softsimon
de1e6d3b27 Merge pull request #3672 from mempool/hunicus/unchained-icon
Update unchained icon on about page
2023-05-06 01:18:53 +04:00
softsimon
9c9837d722 Merge pull request #3720 from mempool/simon/fix-global-footer-graph-pages
Fix height on graphs pages with footer
2023-05-06 00:55:56 +04:00
softsimon
494ceca44e Correcting graph widget bottom padding 2023-05-06 00:55:25 +04:00
softsimon
cb324733bf Removing weird bottom padding 2023-05-06 00:31:08 +04:00
softsimon
ad6d817f32 Adding bottom margin on some graph list pages 2023-05-06 00:20:56 +04:00
softsimon
e5ae2f6ef6 Fixing header margin and Lightning charts 2023-05-06 00:08:26 +04:00
softsimon
3425f2e390 Fix height on graphs pages with footer 2023-05-05 23:38:35 +04:00
Mononaut
3d0f7d6855 add missing rbf eviction 2023-05-05 10:20:17 -07:00
Mononaut
d322c6b5b5 Fix new block mempool deletion race condition 2023-05-05 10:19:11 -07:00
softsimon
a7dff0effe Merge pull request #3719 from mempool/mononaut/fix-rbf-cache-eviction
fix rbf cache eviction logic
2023-05-05 14:27:56 +04:00
Mononaut
f456912679 fix rbf cache eviction logic 2023-05-04 23:30:11 -04:00
softsimon
0d204426e6 Merge pull request #3564 from mempool/simon/global-footer
Global footer
2023-05-05 02:14:36 +04:00
softsimon
4c5ece8249 Fixing navbar overflowing footer 2023-05-05 02:06:48 +04:00
softsimon
e7ae9049bb Enabling footer on about page 2023-05-05 01:49:11 +04:00
softsimon
d40344aa92 Global footer component that fixes Liquid 2023-05-05 01:38:58 +04:00
softsimon
e3e273a688 Fixing position on mobile view 2023-05-05 00:53:21 +04:00
softsimon
e9a0be4941 Remove logger
Co-authored-by: Bufo <32884105+bufo24@users.noreply.github.com>
2023-05-05 00:53:21 +04:00
softsimon
9ca9ab63f5 Bottom padding for mobile 2023-05-05 00:53:20 +04:00
softsimon
261241fcc8 Global footer 2023-05-05 00:53:20 +04:00
softsimon
3c108a271d Merge pull request #3703 from mempool/mononaut/delayed-disk-cache
delay writing disk cache until block handler completes
2023-05-04 18:15:11 +04:00
Mononaut
e5f97ace8b delay writing disk cache until block handler completes 2023-05-03 15:53:47 -06:00
softsimon
e807b3ca74 Merge pull request #3705 from mempool/mononaut/increase-websocket-timeout
Increase client websocket timeout
2023-05-04 01:35:16 +04:00
softsimon
90154aec83 Merge pull request #3380 from mempool/mononaut/mempool-effective-rates
Use effective fee rates in mempool block visualizations & tooltips
2023-05-04 01:19:40 +04:00
Mononaut
d7333ec858 display fee box skeleton while mempool not in sync 2023-05-03 15:01:07 -06:00
softsimon
e6e90799ef Merge branch 'master' into mononaut/mempool-effective-rates 2023-05-04 00:58:49 +04:00
softsimon
8fd9c1a292 Merge pull request #3673 from mempool/mononaut/stable-mempool-positions
Improve stability of the mempool transaction marker arrow
2023-05-04 00:55:35 +04:00
Mononaut
489470639a remove mempool marker on tx confirmation 2023-05-03 14:18:07 -06:00
Mononaut
b79377d5a1 Use new mempool position data for transaction ETA 2023-05-03 14:18:07 -06:00
Mononaut
a22703d547 Add mempool position improvements to updateMempoolBlocks 2023-05-03 14:18:07 -06:00
Mononaut
3b8bcc4da5 Improve stability of mempool tx position arrow 2023-05-03 14:18:06 -06:00
softsimon
a5b764fb66 Merge pull request #3714 from mempool/mononaut/fix-tx-eta
Fix transaction ETA calculation
2023-05-04 00:17:09 +04:00
Mononaut
9bd968f6c3 fix tx page mempool blocks subscription leak 2023-05-03 13:58:08 -06:00
Mononaut
e3f1fced99 keep ETA relative time basis updated 2023-05-03 13:55:26 -06:00
softsimon
12ae940ed6 Merge pull request #2847 from mempool/mononaut/rbf-timeline
RBF Timelines
2023-05-03 23:48:57 +04:00
Mononaut
b325f8c524 Fix import path 2023-05-03 13:16:27 -06:00
Mononaut
ca7c8906a5 Fix missing null checks in tx component 2023-05-03 13:00:23 -06:00
Mononaut
a3b0c56182 Improve RBF diagram state visibility 2023-05-03 13:00:23 -06:00
Mononaut
f29a56f3fe Don't show RBF replaced alert banner after confirmation 2023-05-03 13:00:23 -06:00
Mononaut
36c7697c2b Update rbf disk caching for new method 2023-05-03 13:00:23 -06:00
Mononaut
3355419761 fix missing FULL_RBF_ENABLED config entries 2023-05-03 13:00:22 -06:00
Mononaut
3cb96b32a6 fix rbf e2e test 2023-05-03 13:00:22 -06:00
Mononaut
8d57aa1f06 scroll selected rbf node into view 2023-05-03 13:00:22 -06:00
Mononaut
f2749c67f3 change rbf subheading, fix interval alignment 2023-05-03 13:00:22 -06:00
Mononaut
f95da34fd1 change rbf tooltip to standard two-column table design 2023-05-03 13:00:22 -06:00
Mononaut
7e9cfa0858 Persist RBF cache to disk 2023-05-03 13:00:20 -06:00
Mononaut
6fb4adc27d fixes for non-dual-node rbf feature 2023-05-03 12:53:56 -06:00
Mononaut
723212c918 add mouseover tooltips to rbf timelines 2023-05-03 12:53:56 -06:00
Mononaut
086b41d958 support trees of RBF replacements 2023-05-03 12:53:56 -06:00
Mononaut
c064ef6ace remove 'replaces' alert on transaction page 2023-05-03 12:53:56 -06:00
Mononaut
f46296a2bb new page listing recent RBF events 2023-05-03 12:53:53 -06:00
Mononaut
7b2a1cfd10 update RBF timeline over websocket 2023-05-03 12:52:21 -06:00
Mononaut
1b843da785 Timeline of replacements for RBF-d transactions 2023-05-03 12:52:20 -06:00
softsimon
8db7326a5a Merge pull request #3709 from mempool/mononaut/optimize-new-block-gbt
skip unnecessary makeBlockTemplates call
2023-05-03 21:56:07 +04:00
Mononaut
3f49944c05 Fix transaction ETA calculation 2023-05-03 10:02:03 -06:00
softsimon
2f0d4d6068 Merge pull request #3712 from mempool/revert-3694-simon/revert-tcp-socket-fallback
Revert "Revert TCP socket fallback"
2023-05-03 10:12:59 +04:00
softsimon
dd68572603 Revert "Revert TCP socket fallback" 2023-05-03 10:11:44 +04:00
Mononaut
03ee5c7c31 skip unnecessary makeBlockTemplates 2023-05-02 18:47:34 -06:00
softsimon
4d5662fee4 Merge pull request #3708 from mempool/simon/change-forensics-logging-to-debug
Change forensic logging to debug
2023-05-02 18:08:26 +04:00
softsimon
565aa9616b Change forensic logging to debug 2023-05-02 17:39:02 +04:00
softsimon
62ef1768ec Merge pull request #3707 from mempool/simon/audit-optimization-dead-code
Removing dead code causing slowdown
2023-05-02 16:37:54 +04:00
softsimon
c659adb4be Removing dead code causing slowdown 2023-05-02 15:40:16 +04:00
Mononaut
3691ba8242 Increase client websocket timeout 2023-05-01 18:13:53 -06:00
softsimon
32c39f7af9 Merge pull request #3702 from mempool/mononaut/websocket-logs
Log websocket statistics
2023-05-02 00:55:13 +04:00
Mononaut
3748102bb0 Log websocket statistics 2023-05-01 13:08:29 -06:00
softsimon
a87b604153 Merge pull request #3700 from mempool/mononaut/fix-async-cache-load
await for mempool change handler after loading disk cache
2023-05-01 01:54:33 +04:00
Mononaut
4597bfa5d7 use $ naming convention for async function names 2023-04-30 15:52:44 -06:00
Mononaut
f30cf70226 await for mempool change handler after loading disk cache 2023-04-30 15:51:26 -06:00
softsimon
ba4253da79 Merge pull request #3689 from mempool/mononaut/debug-main-loop-stall
detect and log stalls in the main loop
2023-05-01 00:17:57 +04:00
softsimon
58b08f2c33 Add end quotes 2023-05-01 00:16:23 +04:00
softsimon
ac240398ef Merge branch 'master' into mononaut/debug-main-loop-stall 2023-04-30 22:53:06 +04:00
softsimon
1f2d05c5a4 Merge pull request #3696 from mempool/mononaut/mysql-timeout
Add explicit timeout to mysql DB queries
2023-04-30 22:09:02 +04:00
Mononaut
e05f2198d5 Add explicit timeout to mysql DB queries 2023-04-28 19:21:03 -06:00
Mononaut
95df317f56 detect and log stall in main loop 2023-04-28 19:17:58 -06:00
softsimon
f61f520a4b Merge pull request #3687 from mempool/simon/backend-block-tip-height-endpoint
Backend block tip height endpoint
2023-04-28 13:41:49 +04:00
wiz
864225a0dc Merge branch 'master' into simon/backend-block-tip-height-endpoint 2023-04-28 18:25:13 +09:00
softsimon
5628da2f80 Merge pull request #3694 from mempool/simon/revert-tcp-socket-fallback
Revert TCP socket fallback
2023-04-28 12:16:02 +04:00
softsimon
000c46bf57 Revert TCP socket fallback 2023-04-28 12:06:49 +04:00
softsimon
66919a1aba Backend block tip height endpoint 2023-04-26 13:49:01 +04:00
hunicus
008ec104b6 Fix clashing class in unchained svg 2023-04-19 22:52:53 -04:00
hunicus
b0859f91b2 Update unchained icon on about page 2023-04-19 22:00:09 -04:00
softsimon
1df2c89cdb Merge branch 'master' into mononaut/fix-mempool-block-skeleton 2023-04-12 00:11:51 +08:00
softsimon
2fbe2b2fa6 Merge pull request #3652 from mempool/mononaut/fix-flying-mempool-blocks
Disable mempool block animations except when new block is mined
2023-04-08 23:09:53 +08:00
softsimon
d293d637b5 Merge pull request #3633 from mempool/mononaut/right-click-scroll
Disable blockchain drag for middle/right click
2023-04-08 17:00:25 +08:00
softsimon
04a8249883 Merge pull request #3644 from mempool/mononaut/full-mempool-cpfp
Perform full cpfp calculations for the entire mempool
2023-04-08 16:50:57 +08:00
softsimon
a196c276c9 Merge pull request #3656 from mempool/nymkappa/update-tests
[config] add missing RETRY_UNIX_SOCKET_AFTER
2023-04-08 10:22:08 +08:00
nymkappa
dfe2cf631f [config] fix docker esplora config and template 2023-04-08 10:42:08 +09:00
nymkappa
d6913b6439 [config] add missing RETRY_UNIX_SOCKET_AFTER 2023-04-07 13:28:32 +09:00
Mononaut
6602bddb2b Fit mempool block skeleton loaders to screen 2023-04-07 03:25:02 +09:00
Mononaut
32cd8bb3cb Prevent mempool block animations except when new block mined 2023-04-07 02:18:33 +09:00
Mononaut
5950034f53 Perform full cpfp calculations for the entire mempool 2023-04-07 00:25:45 +09:00
wiz
d18ebdfc59 ops: Update hard-coded path for liquid asset icons 2023-04-06 19:19:30 +09:00
wiz
604c3ba266 ops: Tweak boot-time delays for all daemons 2023-04-06 19:19:28 +09:00
wiz
b8d063a4f7 Merge pull request #3649 from mempool/nymkappa/indexing-error
Fix indexing error
2023-04-06 17:52:58 +09:00
wiz
3c30415982 ops: Add fallback TCP socket for esplora backends 2023-04-06 17:52:17 +09:00
nymkappa
c5252dc27d [indexing] delete dead code 2023-04-06 11:55:25 +09:00
nymkappa
6016db2533 [indexing] save missing fee_percentiles and median_fee_amt when indexing on the fly 2023-04-06 11:55:17 +09:00
nymkappa
b23f14b798 [indexing] fix typescript issue, reading invalid field 2023-04-06 11:54:22 +09:00
wiz
09d52f9fbe Merge pull request #3643 from mempool/nymkappa/esplora-socket-fallback
[esplora] fallback to tcp socket if unix socket fails
2023-04-05 22:58:34 +09:00
nymkappa
c23e529f0a [main loop] retry every seconds upon exception - warn after 5 attempts 2023-04-05 22:44:01 +09:00
nymkappa
ab7cb5f681 [esplora] reset timeout variable when retrying unix socket 2023-04-05 17:05:23 +09:00
nymkappa
db27e5a92c [esplora] print log when retrying unix socket - don't fallback to tcp socket on ETIMEDOUT 2023-04-05 17:00:53 +09:00
wiz
66109afb0d ops: Enable unix socket for esplora on mainnet-lightning 2023-04-05 16:48:26 +09:00
nymkappa
b6f1fd5a4a [esplora] initialize default socket config to axiosConfigWithUnixSocket 2023-04-05 16:38:37 +09:00
nymkappa
44a0913b81 [esplora] fallback to tcp socket if unix socket fails 2023-04-05 16:27:13 +09:00
Mononaut
4c569c0ded Send mempool effective fee rate changes to frontend & apply 2023-04-05 08:42:01 +09:00
Mononaut
3d5c156776 Use effective fee rates in mempool block visualizations & tooltips 2023-04-05 08:42:01 +09:00
wiz
6c81dcdc76 Merge pull request #3640 from mempool/ops/use-unix-sockets-for-mysql
ops: Use unix sockets for MySQL
2023-04-04 21:52:26 +09:00
wiz
906f24f0ee ops: Use unix sockets for MySQL 2023-04-04 21:48:42 +09:00
wiz
bca35600ff ops: Fix installer creation of CLN folders 2023-04-04 20:18:46 +09:00
wiz
a9dc5e9be4 Merge pull request #3634 from mempool/nymkappa/fiat-component-custom-color
Make fiat component color class customizable
2023-04-04 14:11:58 +09:00
nymkappa
90fa4a8f77 Make fiat component color class customizable 2023-04-04 11:42:06 +09:00
Mononaut
d325734c16 Disable blockchain drag for middle/right click 2023-04-04 08:25:40 +09:00
wiz
bdbb1dcf8e Merge pull request #3631 from mempool/simon/improved-warning-message
Redesigned testnet alert
2023-04-03 22:09:52 +09:00
wiz
2ada9dcd40 Merge branch 'master' into simon/improved-warning-message 2023-04-03 21:58:09 +09:00
wiz
95cc74c076 Merge pull request #3630 from mempool/hunicus/phx-dark-icon
Switch phoenix wallet logo to dark mode
2023-04-03 21:57:56 +09:00
softsimon
d59a31a65a Merge branch 'master' into simon/improved-warning-message 2023-04-03 20:03:04 +09:00
softsimon
38e4832b6a Restoring missing tx index attribute for cypress 2023-04-03 20:02:39 +09:00
softsimon
6b6dc9fb24 Merge pull request #3622 from mempool/mononaut/shift-click
Key modifiers to open transaction in new tab from visualization
2023-04-03 19:05:08 +09:00
softsimon
a1b6fc5a7b Redesigned testnet alert
fixes #3625
2023-04-03 18:30:06 +09:00
wiz
6ac0e887f7 Merge pull request #3247 from mempool/ops/esplora-unix-sockets
ops: Use unix sockets to query esplora from nginx
2023-04-03 16:07:05 +09:00
wiz
bdb7e62921 Merge branch 'master' into ops/esplora-unix-sockets 2023-04-03 15:34:47 +09:00
hunicus
445e376675 Switch phoenix wallet logo to dark mode 2023-04-03 02:07:56 -04:00
wiz
bedbd9c5d5 Merge pull request #3587 from mempool/hunicus/mynode-casing
Update mynode profile on about page
2023-04-03 15:00:13 +09:00
wiz
34236fca7c Remove unused mynodebtc.jpg 2023-04-03 14:59:39 +09:00
wiz
f74d651b85 Merge branch 'master' into hunicus/mynode-casing 2023-04-03 14:51:28 +09:00
softsimon
41a93af89e Merge pull request #3601 from mempool/mononaut/fix-liquid-asset-diagram
Fix broken tx diagram for non-LBTC liquid assets
2023-04-03 14:39:38 +09:00
wiz
e5b1615c61 Merge branch 'master' into mononaut/fix-liquid-asset-diagram 2023-04-03 12:27:32 +09:00
softsimon
2ef340712f Merge pull request #3442 from mempool/nymkappa/reorg-keep-templates
When a re-org happens, keep the block templates for audit
2023-04-03 12:24:05 +09:00
wiz
3841d1e7b8 Merge pull request #3600 from mempool/mononaut/fix-unfurl-cpfp-badge
Fix unfurl cpfp badge
2023-04-03 11:50:11 +09:00
wiz
675ecc608c Merge branch 'master' into mononaut/fix-unfurl-cpfp-badge 2023-04-03 11:40:47 +09:00
wiz
3625e41e97 Merge pull request #3599 from mempool/mononaut/fix-ln-node-unfurl
Fix node unfurl row overflow
2023-04-03 11:40:39 +09:00
wiz
ff8fecbd05 Merge branch 'master' into mononaut/fix-ln-node-unfurl 2023-04-03 11:31:20 +09:00
Mononaut
a91a8d2a4b Key modifiers to open tx in new tab from visualization 2023-04-02 07:46:32 +09:00
softsimon
83c03474a9 Merge pull request #3586 from mempool/nymkappa/fix-price-undefined
Add missing sanity check when fetching single price datapoint
2023-04-01 18:04:32 +09:00
softsimon
f55aac46f1 Merge pull request #3568 from mempool/hunicus/fix-memfaq-mobile
Fix anchor link expand on mobile for mempool faq
2023-04-01 18:03:17 +09:00
softsimon
f1b5ee2a5f Merge pull request #3404 from mempool/nymkappa/bugfix/wrong-percentage-heap-log
Fix % on heap limit warn
2023-04-01 16:56:50 +09:00
softsimon
97008b9caa Merge pull request #3326 from mempool/nymkappa/warning-testnet-signet
Show warning on testnet/signet
2023-04-01 16:53:17 +09:00
softsimon
b3038e557c Merge pull request #3618 from mempool/nymkappa/ln-stats-import-trycatch
Wrap lightning stats importer into try/catch
2023-04-01 15:21:55 +09:00
softsimon
61e29bcff9 Merge pull request #3608 from mempool/nymkappa/fix-default-graph-preference
Use window.location object instead of angular router for default graph window preference setting
2023-04-01 15:18:10 +09:00
nymkappa
a512884b65 Wrap lightning stats importer into try/catch 2023-04-01 14:56:18 +09:00
softsimon
46fbd6aa49 Merge pull request #3443 from mempool/simon/update-backend-libs-2023-03
Update backend NPM deps
2023-04-01 12:25:40 +09:00
softsimon
fc29943d0f Upgrading deps 2023-04-01 12:16:59 +09:00
softsimon
482a609d84 Update backend NPM libs 2023-04-01 12:15:32 +09:00
softsimon
b7d869ad23 Merge pull request #3375 from mempool/nymkappa/log-update
Update some logs
2023-04-01 12:07:53 +09:00
nymkappa
321161ede9 Cleanup some log 2023-04-01 12:00:54 +09:00
softsimon
b5ad0895ac Merge pull request #3610 from mempool/nymkappa/fix-search-wiz-test
Fix search 1wizS test
2023-03-31 22:32:40 +09:00
softsimon
427cef9f9d Merge pull request #3611 from mempool/nymkappa/fix-transaction-list-infinite-scroll
Fix infinite scroll transaction list component
2023-03-31 19:23:36 +09:00
nymkappa
816fb3bf01 Don't delete transactions when checking if the current chain is valid 2023-03-31 12:22:26 +09:00
nymkappa
44bbb472d3 Keep re-org'ed block summaries in the database 2023-03-31 12:08:05 +09:00
nymkappa
aba49897f9 Fix infinite scroll transaction list component 2023-03-30 17:07:34 +09:00
nymkappa
96121a86f8 Fix search 1wizS test 2023-03-29 17:35:49 +09:00
nymkappa
ea2193a42d Add missing sanity check when fetching single price datapoint 2023-03-29 17:33:07 +09:00
nymkappa
9e4fe40ca3 When a re-org happens, keep the block templates for audit 2023-03-29 17:32:17 +09:00
nymkappa
d9b4ad64bb Fix % on heap limit warn 2023-03-29 17:30:32 +09:00
nymkappa
7562407a0c Show warning on testnet/signet 2023-03-29 17:27:33 +09:00
nymkappa
0bc244b9f1 Use window.location object instead of angular router for default graph window preference setting 2023-03-29 15:10:59 +09:00
Mononaut
14e0d80042 Fix broken tx diagram for non-lBTC liquid assets 2023-03-29 07:54:58 +09:00
Mononaut
5555916de3 Fix unfurl cpfp badge 2023-03-29 05:45:49 +09:00
Mononaut
ef09912d1b Fix node unfurl row overflow 2023-03-29 03:15:01 +09:00
softsimon
5977251a20 Merge pull request #3316 from mempool/mononaut/projected-median-fee
New median fee calculation for mempool blocks
2023-03-28 17:22:20 +09:00
Mononaut
a4c027dc48 clean up unused vars in mempool-blocks.ts 2023-03-28 17:02:37 +09:00
Mononaut
9f40cba914 use new median fee calculation for mempool blocks 2023-03-28 17:02:37 +09:00
softsimon
5ba2c181b0 Merge pull request #3315 from mempool/mononaut/effective-fee-rates
Use effective fee rate heuristics for block fee span
2023-03-28 16:57:22 +09:00
Mononaut
2fc404a55c refactor effective rate calculation 2023-03-28 16:20:20 +09:00
Mononaut
2baa10dcef Use effective fee rate heuristics for block fee span 2023-03-28 16:19:06 +09:00
softsimon
d08a318a2c Merge pull request #3584 from mempool/release/v2.5.0
Release v2.5.0
2023-03-28 14:26:18 +09:00
wiz
96f3218ec6 Bump version to v2.6.0-dev 2023-03-28 14:25:05 +09:00
wiz
57eddac7f0 Release v2.5.0 2023-03-28 12:14:31 +09:00
wiz
af115b49aa Merge pull request #3585 from mempool/nymkappa/fix-db-state
Reset pools sha db state
2023-03-28 12:12:04 +09:00
softsimon
332f9a2f5e Pull from transifex (Vietnamese) 2023-03-28 12:11:27 +09:00
hunicus
2b3d132db6 Update mynode logo 2023-03-27 07:26:06 -04:00
hunicus
f1361a698d Switch mynode capitalization to match branding 2023-03-27 06:50:30 -04:00
nymkappa
34eef3553b Reset pools sha db state 2023-03-27 19:39:50 +09:00
softsimon
9e4ce42b6a Pull from transifex (Hebrew) 2023-03-27 16:31:02 +09:00
softsimon
4c4a91ae95 Merge pull request #3560 from mempool/mononaut/missing-tx-bug
Fix thread inconsistency / lazy deletion race condition bugs
2023-03-27 15:33:34 +09:00
softsimon
93d46d5c5b Pull from transifex 2023-03-27 15:12:23 +09:00
softsimon
8788d4f898 Pull from transifex 2023-03-27 15:10:17 +09:00
wiz
e28650c46c Merge pull request #3581 from mempool/simon/enable-lightning-mempool-prod 2023-03-27 14:53:46 +09:00
softsimon
855c11f02c Enabling mempool in lightning prod
fixes #3579
2023-03-27 14:51:34 +09:00
softsimon
3f8e91bd46 Merge pull request #3578 from mempool/nymkappa/revert-undocumented-fast-forward
Revert regression introduced in #1320
2023-03-26 22:10:23 +09:00
softsimon
6722e45109 Merge pull request #3576 from mempool/mononaut/fix-difficulty-adjustment
Fix difficulty adjustment bugs
2023-03-26 18:02:30 +09:00
nymkappa
414383638d Revert regression introduced in #1320 2023-03-26 17:54:24 +09:00
nymkappa
2575b79c05 Merge branch 'master' into mononaut/fix-difficulty-adjustment 2023-03-26 17:02:41 +09:00
nymkappa
c7cab4c877 Remove difficulty adjustment calculation lag in the backend 2023-03-26 17:01:04 +09:00
softsimon
85c2f0ba30 Pull from transifex 2023-03-26 16:46:20 +09:00
Mononaut
edfbede704 Don't send back difficulty adjustment info 2023-03-26 09:05:41 +09:00
Mononaut
5f60cb821a Fix difficulty adjustment start-of-epoch edge cases 2023-03-26 07:27:11 +09:00
Mononaut
8486c1117d log warnings for unexpectedly missing txs 2023-03-26 05:41:31 +09:00
hunicus
ad3785ff41 Fix anchor link expand on mobile for mempool faq 2023-03-24 21:22:49 -04:00
Mononaut
61f24562fd tighten sanity checks in block audit 2023-03-24 09:49:02 +09:00
Mononaut
28de93d0ff move lazy tx deletion into main loop 2023-03-24 09:48:08 +09:00
Mononaut
1fd85b729d handle stale transactions in block templates 2023-03-24 09:47:08 +09:00
softsimon
5681ae3f5c Pull from transifex 2023-03-23 22:45:07 +09:00
softsimon
9d9e0976ae Pull from transifex 2023-03-23 17:41:24 +09:00
wiz
6180837636 Merge pull request #3557 from mempool/nymkappa/hotfix-infinite-scroll
Hotfix infinite scroll (need to apply a real fix for 2.5.1)
2023-03-23 17:36:08 +09:00
wiz
17beaf7d4f Merge pull request #3555 from mempool/hunicus/raspiblitz-logo
Change raspibolt logo to raspiblitz logo
2023-03-23 17:34:01 +09:00
wiz
ce8f471b27 Merge pull request #3554 from mempool/simon/bumping-electrum-client
Bumping electrum-client
2023-03-23 17:27:31 +09:00
nymkappa
b3e36fdd99 Hotfix infinite scroll (need to apply a real fix) 2023-03-23 17:23:32 +09:00
wiz
f971ddf1fa Merge branch 'master' into hunicus/raspiblitz-logo 2023-03-23 17:22:18 +09:00
hunicus
c0c37922c3 Change raspibolt logo to raspiblitz logo 2023-03-23 03:12:44 -04:00
wiz
1eb9e58331 Merge branch 'master' into simon/bumping-electrum-client 2023-03-23 15:58:44 +09:00
wiz
f8a35a110c Merge pull request #3553 from mempool/nymkappa/electrum-retry
Reconnect to electrum an unlimited amount of times every 1 seconds upon disconnection
2023-03-23 15:58:37 +09:00
softsimon
c4d13fb5b7 Bumping electrum-client 2023-03-23 15:56:30 +09:00
nymkappa
53a44853b3 Reconnect to electrum an unlimited amount of times every 1 seconds up disconnection 2023-03-23 15:18:48 +09:00
softsimon
29aa3617d8 Crediting Lithuanian and Danish translator 2023-03-23 14:43:03 +09:00
wiz
addf3e2521 Merge pull request #3548 from mempool/simon/updating-node-map-loading
Update channels map indexing indicator
2023-03-23 13:46:34 +09:00
softsimon
5826f8fa1e Pull from transifex 2023-03-23 00:28:17 +09:00
wiz
965d89fd91 Merge branch 'master' into simon/updating-node-map-loading 2023-03-22 17:48:09 +09:00
nymkappa
ed69591bcf Show "No data to display yet" in "Fee distribution" chart on node page when there are no channels yet 2023-03-22 14:09:30 +09:00
nymkappa
f1f6c48128 Show "No data to display yet" until we have at least two points for node stats charts 2023-03-22 13:45:27 +09:00
softsimon
f8bd062aa2 Pull from transifex 2023-03-22 13:35:16 +09:00
softsimon
77835bcb9d Restoring Preview component behavior 2023-03-22 13:20:22 +09:00
softsimon
bf5821c8c8 Remove indexing indicator 2023-03-21 23:17:09 +09:00
softsimon
a2e23014f4 Update channels map indexing indicator 2023-03-21 23:14:45 +09:00
wiz
811c14a6bd Merge pull request #3547 from mempool/simon/node-fee-chart-loading-d
Removing "d" from node fee chart loading
2023-03-21 23:10:47 +09:00
wiz
a34d87148b Merge branch 'master' into simon/node-fee-chart-loading-d 2023-03-21 23:01:19 +09:00
wiz
a45a8db479 Merge pull request #3494 from mempool/simon/difficulty-mining-css-updates
Difficulty mining css updates
2023-03-21 23:00:54 +09:00
softsimon
672f71c515 Removing "d" from node fee chart loading 2023-03-21 22:48:40 +09:00
softsimon
2c16bbb0e9 Fixing sub text height 2023-03-21 22:26:18 +09:00
softsimon
63f7709e82 Restoring size of current change 2023-03-21 22:20:14 +09:00
wiz
15b13ef4a4 Merge branch 'master' into simon/difficulty-mining-css-updates 2023-03-21 22:03:46 +09:00
wiz
75303c7a34 docker: Minor tweak to frontend entrypoint LND detection 2023-03-21 21:50:46 +09:00
softsimon
1a6048f0ab Difficulty mining css updates 2023-03-21 21:25:37 +09:00
wiz
ae6a408c05 Merge pull request #3493 from mempool/simon/update-softsimon-profile
Update softsimon profile photo
2023-03-21 20:58:11 +09:00
wiz
1015cbfa94 Merge pull request #3492 from mempool/simon/lightning-indexing-status
Lightning indexing indicators
2023-03-21 20:56:47 +09:00
wiz
876feef53f Fix frontend docker entrypoint umbrel LND detection 2023-03-21 18:11:10 +09:00
softsimon
f5f0329d39 Update softsimon profile photo 2023-03-21 18:03:19 +09:00
wiz
80a7b6d8d5 Merge branch 'master' into simon/lightning-indexing-status 2023-03-21 18:02:28 +09:00
wiz
f72e17c12e Merge pull request #3491 from mempool/simon/audit-off-hide-health
Audit disabled related UX fixes
2023-03-21 18:02:24 +09:00
wiz
f570b2762f Merge branch 'master' into simon/audit-off-hide-health 2023-03-21 17:43:56 +09:00
wiz
e2fda99578 Merge pull request #3490 from mempool/simon/auto-disable-ln-on-macaroon-fail
Auto disable LN on macaroon fail
2023-03-21 17:43:12 +09:00
softsimon
d7d45146c8 Lightning indexing indicators
refs  #2647
2023-03-21 17:33:14 +09:00
softsimon
45dbc6c6f6 Update logger network after modifying config 2023-03-21 16:21:11 +09:00
softsimon
d76e3a5939 Audit disabled related UX fixes 2023-03-21 16:02:46 +09:00
wiz
cb8fdb5e8d Hack docker frontend entrypoint to auto-enable lightning 2023-03-21 15:57:22 +09:00
softsimon
d337bf3ee2 Turn off LN if Macaroon is missing 2023-03-21 15:52:41 +09:00
softsimon
758e4d4f4c Disable LN on macaroon fail 2023-03-21 15:49:38 +09:00
wiz
ccab8b16bf Merge branch 'master' into ops/esplora-unix-sockets 2023-03-21 14:29:06 +09:00
softsimon
cd2bda4b49 Pull russian from transifex 2023-03-20 21:44:47 +09:00
wiz
493ea0641d Merge pull request #3487 from mempool/simon/catch-unhandled-lnd-axios-request
Catch exeptions in Lightning stats
2023-03-20 21:42:47 +09:00
wiz
ca1b6553c9 Merge branch 'master' into simon/catch-unhandled-lnd-axios-request 2023-03-20 20:53:41 +09:00
wiz
d479715d8e Merge pull request #3450 from mempool/nymkappa/configurable-timeout
Make core and lnd rpc calls timeout configurable
2023-03-20 20:50:51 +09:00
softsimon
e3109a8fec Catch exeptions in Lightning stats
fixes #3486
2023-03-20 20:46:11 +09:00
softsimon
e6bc5bef33 Updating russian i18n 2023-03-20 18:50:20 +09:00
softsimon
d82a7169b7 Pull from transifex 2023-03-20 18:28:15 +09:00
wiz
ba48b6f7ce Merge branch 'master' into nymkappa/configurable-timeout 2023-03-20 18:26:04 +09:00
wiz
8e1cf997f7 Merge pull request #3451 from mempool/simon/mining-difficulty-overflow-fix
Difficulty mining ellipsis fix
2023-03-20 18:25:51 +09:00
wiz
70d8548c92 Merge branch 'master' into simon/mining-difficulty-overflow-fix 2023-03-20 17:45:27 +09:00
wiz
cce7dd917f Merge branch 'master' into nymkappa/configurable-timeout 2023-03-20 17:38:40 +09:00
wiz
3dafb284a9 Merge pull request #3447 from mempool/hunicus/readme-video-poster
Add poster image for readme video
2023-03-20 17:37:21 +09:00
wiz
6a599a9a30 Merge pull request #3448 from mempool/simon/fix-missing-temp-cache-disk-cache
Fix missing temp cache in disk cache
2023-03-20 17:33:41 +09:00
softsimon
74fb292633 Difficulty mining ellipsis fix 2023-03-20 17:21:34 +09:00
nymkappa
c6e063ea2f Make lnd timeout configurable 2023-03-20 16:35:44 +09:00
nymkappa
81d563381a Make bitcoin core timeout configurable 2023-03-20 16:15:40 +09:00
softsimon
870e895144 Correcting docker disk cache config variable 2023-03-20 16:12:56 +09:00
softsimon
343d1345e2 Merge pull request #3445 from mempool/simon/liquid-blinding-tests-failing
Fixing broken liquid blinding tests
2023-03-20 15:46:41 +09:00
softsimon
517cf613c1 Removing Sigterm. Cache write block interval configuration. 2023-03-20 15:46:05 +09:00
softsimon
d54bcc898b Fix missing temp cache in disk cache 2023-03-20 15:44:55 +09:00
wiz
704e1741ed Merge pull request #3449 from mempool/nymkappa/fix-tests 2023-03-20 14:22:01 +09:00
nymkappa
ad5ce6dba4 Fix maxmind tests 2023-03-20 14:02:31 +09:00
hunicus
1ed20a95df Add poster image for readme video
Also add line-break after video and remove screenshot.
2023-03-19 22:58:39 -04:00
wiz
1718ddd4c3 Merge pull request #3444 from mempool/nymkappa/missing-docker
Update docker configs
2023-03-19 18:56:06 +09:00
wiz
32c2db2153 Fix backend docker config path for GeoIP data 2023-03-19 18:42:38 +09:00
wiz
0abb9cbb7c Fix boolean configuration option in docker backend config 2023-03-19 17:49:37 +09:00
wiz
e2e71c7a46 Add Maxmind GeoIP Lite download to Docker build 2023-03-19 17:49:08 +09:00
softsimon
e27bdd3e2b Fixing broken liquid blinding tests 2023-03-19 17:30:06 +09:00
softsimon
5839ed428e Merge pull request #3378 from mempool/nymkappa/responsive
Fix block list component responsive
2023-03-19 16:48:47 +09:00
nymkappa
af6d115dbb Add missing MAXMIND in docker configs - Remove duplicated __MEMPOOL_INDEXING_BLOCKS_AMOUNT__ 2023-03-19 15:39:17 +09:00
nymkappa
194968d16f Fix block list component responsive 2023-03-19 15:23:00 +09:00
softsimon
30686bd322 Updating korean translation 2023-03-19 12:32:07 +09:00
softsimon
175c645777 Crediting korean translator 2023-03-19 12:05:13 +09:00
wiz
587a259843 Merge pull request #3440 from mempool/ops/bump-elements-v22.1
ops: Bump elementsd to v22.1
2023-03-18 20:06:53 +09:00
softsimon
64749ca726 Pull from transifex 2023-03-18 19:39:07 +09:00
wiz
8f2493dadb ops: Use old elements RPC port 7040 2023-03-18 18:48:37 +09:00
softsimon
7d8ea075d9 Merge pull request #3405 from mempool/nymkappa/pool-health
Show block health in pool block list
2023-03-18 18:48:10 +09:00
wiz
328327e5dc Merge pull request #3104 from mempool/mononaut/liquid-tooltip-fees
Fix missing fees in liquid block tooltips
2023-03-18 18:33:17 +09:00
wiz
fd1816d451 Merge pull request #3402 from mempool/hunicus/promo-subtitles
Add subtitles for promo video
2023-03-18 18:27:35 +09:00
hunicus
72f25b873c Add zh subtitles for promo video 2023-03-18 05:17:23 -04:00
hunicus
994656953c Fix promo video subtitles for sv 2023-03-18 05:15:48 -04:00
Mononaut
ed46232b83 Fix missing fees in liquid block tooltips 2023-03-18 18:11:10 +09:00
wiz
dca18a1c66 Merge branch 'master' into hunicus/promo-subtitles 2023-03-18 17:59:03 +09:00
wiz
c291ee1789 Merge pull request #3407 from mempool/hunicus/mempool-size-faq
Add faqs on mempool size and memory usage
2023-03-18 17:58:56 +09:00
hunicus
caf15351f7 Merge branch 'master' into hunicus/promo-subtitles 2023-03-18 17:58:29 +09:00
hunicus
6be9f23790 Add more languages for promo captions 2023-03-18 04:57:20 -04:00
nymkappa
adc51f6217 Update i18n 2023-03-18 16:40:30 +09:00
softsimon
ec8a46ede6 Pulling from transifex 2023-03-18 15:33:57 +09:00
softsimon
b78fdf5a23 Merge pull request #3353 from mempool/mononaut/mempool-block-animations
Improve mempool block animations
2023-03-18 12:46:20 +09:00
wiz
7c2493f3fa ops: Bump elementsd to v22.1 2023-03-17 22:07:48 +09:00
hunicus
b0a0ad11b4 Show mempool.space memory usage faq on official 2023-03-17 21:25:28 +09:00
hunicus
377f71eb52 Integrate feedback to memory usage faqs 2023-03-17 21:25:28 +09:00
hunicus
eefe343973 Add faqs on mempool size and memory usage 2023-03-17 21:25:28 +09:00
softsimon
41a6674fad Merge pull request #3379 from mempool/nymkappa/testnet-signet-price-zero
Don't fetch prices on signet/testnet, always show 0
2023-03-17 16:40:09 +09:00
softsimon
effba92729 Merge pull request #3381 from mempool/mononaut/network-special-blocks
Limit special blocks by network, add future halvings
2023-03-17 16:39:54 +09:00
wiz
c9f4bdda17 Merge pull request #3408 from knorrium/gha_docker_digest
Docker digest GHA
2023-03-17 14:49:19 +09:00
softsimon
ef54385068 Merge pull request #3413 from mempool/simon/pull-from-transifex-2023-03-17
Pull from transifex
2023-03-17 14:02:08 +09:00
softsimon
b8f77c4be4 Pull from transifex 2023-03-17 14:01:36 +09:00
Mononaut
b5c2073414 Fix getSimilarity error on empty mempool 2023-03-16 22:16:40 +09:00
Mononaut
25aacb5046 Calculate similarity score with audit disabled 2023-03-16 22:16:40 +09:00
Mononaut
c24724dcdf animate mempool blocks conditional on mined block similarity 2023-03-16 22:16:40 +09:00
Mononaut
64ab14f995 mempool block entry animation 2023-03-16 22:16:40 +09:00
nymkappa
4a64c0dfa5 Fix skeleton 2023-03-16 16:35:59 +09:00
nymkappa
0ebe0a5dc9 Add new stats in mining pool page 2023-03-16 16:13:11 +09:00
hunicus
c683a52a01 Show captions for non-english locales 2023-03-16 02:29:33 -04:00
Felipe Knorr Kuhn
5fbdd0bb2a Remove push trigger 2023-03-15 22:48:28 -07:00
Felipe Knorr Kuhn
599881366b Add GHA for Docker digest 2023-03-15 22:43:41 -07:00
hunicus
4c294b010d Make subtitles default to current locale 2023-03-15 23:06:10 -04:00
hunicus
418c32e334 Make git ignore mp4 and vtt files in /resources 2023-03-15 23:06:10 -04:00
hunicus
12b605e5cc Fetch subtitles files from github
For en, sv, and ja.
2023-03-15 23:06:10 -04:00
softsimon
870a7e51b1 Merge pull request #3373 from mempool/mononaut/fix-testnet-signet-features
Network-specific activation heights for transaction feature badges
2023-03-15 18:35:40 +09:00
hunicus
cdfde05452 Make new folder for promo video assets
Video, cover image, and subtitle files.
2023-03-15 02:50:17 -04:00
softsimon
ce24b8bb0a Merge pull request #3331 from mempool/mononaut/disk-cache-network-version
Add network versioning to disk cache
2023-03-15 14:32:28 +09:00
softsimon
1b2810ec0e Merge branch 'master' into mononaut/disk-cache-network-version 2023-03-14 21:05:16 +09:00
softsimon
c8e84ec056 Merge pull request #3329 from mempool/mutiny-integration
Add mutiny as community integration
2023-03-14 19:59:43 +09:00
Mononaut
7bf8fea9f2 Limit special blocks by network, add future halvings 2023-03-14 16:58:02 +09:00
softsimon
3458c5af71 Merge branch 'master' into mononaut/fix-testnet-signet-features 2023-03-14 16:50:06 +09:00
softsimon
3e46dabf7b Merge pull request #3345 from mempool/mononaut/hide-empty-features
Hide features row if tx has no features
2023-03-14 16:48:56 +09:00
nymkappa
a5dd141934 Don't fetch prices on signet/testnet, always show 0 2023-03-14 15:39:15 +09:00
softsimon
881af309ab Pull from transifex 2023-03-14 14:54:49 +09:00
softsimon
374ff50a62 Merge pull request #3376 from mempool/i18n/enable-danish
i18n: Enable Danish
2023-03-14 14:53:00 +09:00
wiz
2b9e63dbc5 i18n: Enable Danish 2023-03-14 14:31:20 +09:00
Mononaut
9f453deceb Use network-specific feature activation dates 2023-03-14 13:02:50 +09:00
softsimon
0b88d94573 Fix channels i18n string on world map
fixes #3336
2023-03-13 18:42:43 +09:00
wiz
536114853c Merge pull request #3351 from mempool/nymkappa/disable-pool-updater-no-mempool
Disable pool update when running lightning only
2023-03-13 18:16:29 +09:00
wiz
4d281277d6 Merge branch 'master' into nymkappa/disable-pool-updater-no-mempool 2023-03-13 18:02:03 +09:00
wiz
c97a722a3b Merge pull request #3350 from mempool/simon/video-height-fix
Correct video height on mobile
2023-03-13 18:01:17 +09:00
softsimon
adacb42d1a Merge branch 'master' into simon/video-height-fix 2023-03-13 17:41:45 +09:00
softsimon
b6427d6f67 Merge branch 'master' into nymkappa/disable-pool-updater-no-mempool 2023-03-13 17:41:04 +09:00
softsimon
433acb7b1d Merge pull request #3352 from mempool/simon/i18n-string-error-fix
Fixes i18n string error
2023-03-13 17:40:52 +09:00
softsimon
d015ee7824 Fixes i18n string error 2023-03-13 17:40:17 +09:00
nymkappa
ecfb980e75 Disable pool update when running lightning only 2023-03-13 17:24:23 +09:00
softsimon
cfdbd30956 Correct video height on mobile
#3342
2023-03-13 17:18:09 +09:00
softsimon
7acfec2406 Merge pull request #3349 from mempool/simon/update-missing-i18n-strings
Fixing more missing i18n keys
2023-03-13 17:08:50 +09:00
softsimon
070ee10fb0 Fixing more missing i18n keys
fixes #3339
fixes #3337
2023-03-13 17:08:27 +09:00
wiz
7970f4ae88 ops: Use unix sockets to query esplora from nginx 2023-03-13 16:35:27 +09:00
Mononaut
96a41400f4 Add axios support for esplora unix sockets 2023-03-13 14:53:44 +09:00
softsimon
446b0de8f3 Merge pull request #3347 from mempool/simon/ninja-i18n-pull
Ninja i18n pull
2023-03-13 13:04:13 +09:00
softsimon
99b7fc8814 Ninja i18n pull 2023-03-13 13:03:57 +09:00
softsimon
defb88a474 Merge pull request #3346 from mempool/simon/i18n-extract-2023-03-13
i18n extract
2023-03-13 12:58:48 +09:00
softsimon
7392535182 i18n extract 2023-03-13 12:58:35 +09:00
Mononaut
130ae8c3a5 Hide features row if tx has no features 2023-03-13 12:48:01 +09:00
softsimon
f4f8b2b271 Merge pull request #3344 from mempool/simon/pull-from-transifex-2023-03-13
Pull from transifex
2023-03-13 12:36:13 +09:00
softsimon
477d09412b Pull from transifex 2023-03-13 12:36:00 +09:00
softsimon
87bc7917d8 Merge pull request #3341 from mempool/mononaut/difficulty-skeleton-width
Fix difficulty skeleton width on firefox
2023-03-13 11:23:17 +09:00
Mononaut
9030d95207 Fix difficulty skeleton width on firefox 2023-03-13 10:28:26 +09:00
softsimon
5e4131b474 Merge pull request #3330 from mempool/mononaut/reduce-disk-cache-frequency
Save cache to disk every 6 blocks
2023-03-12 21:06:19 +09:00
Mononaut
3bf96dafde Add network versioning to disk cache 2023-03-12 19:20:29 +09:00
softsimon
2f4dba895c Merge pull request #3327 from mempool/nymkappa/bugfix/show-total-fee-last-mempool-block
Show cumulated fee on last mempool block
2023-03-12 19:17:57 +09:00
hunicus
a5e281706f Make community integration rows symmetric and full 2023-03-12 06:17:50 -04:00
nymkappa
eba0e1c25a Show cumulated fee on last mempool block 2023-03-12 19:13:39 +09:00
hunicus
97d82042c0 Add mutiny to community integrations 2023-03-12 06:03:23 -04:00
Mononaut
8bd05987e5 Save cache to disk every 6 blocks 2023-03-12 19:03:19 +09:00
wiz
7e676dbaf0 Merge pull request #3325 from mempool/mononaut/fix-liquid-asset-tooltips
Fix units in flow diagram tooltips for liquid assets
2023-03-12 18:31:49 +09:00
softsimon
760f3193d9 Merge pull request #3324 from mempool/hunicus/add-integrations-032023
Add 4 community integrations to about page
2023-03-12 18:15:02 +09:00
wiz
72663b30df Merge branch 'master' into mononaut/fix-liquid-asset-tooltips 2023-03-12 18:14:25 +09:00
hunicus
8995283a58 Remove trailing slash 2023-03-12 05:12:25 -04:00
Mononaut
a8ac6aedf7 Fix units in flow diagram tooltips for liquid assets 2023-03-12 18:07:31 +09:00
wiz
9613247283 Merge branch 'master' into hunicus/add-integrations-032023 2023-03-12 18:03:49 +09:00
hunicus
ab96a17e80 Make non-rounded icons smaller 2023-03-12 04:59:37 -04:00
hunicus
3b080ee5fb Add galoy and boltz to community integrations 2023-03-12 04:52:15 -04:00
hunicus
24a8cca758 Add bitcoin-s and edge to community integrations 2023-03-12 04:51:59 -04:00
softsimon
dbad3af8ba Merge pull request #3322 from mempool/mononaut/fix-new-block-animations
Fix new block animations
2023-03-12 17:42:33 +09:00
softsimon
cdddf3a8b2 Merge pull request #3319 from mempool/mononaut/more-fee-ratings
Show tx fee ratings for older blocks
2023-03-12 17:10:47 +09:00
Mononaut
b675bd8d55 Fix transaction confirmed arrow animation 2023-03-12 17:00:36 +09:00
wiz
cb04d67d07 Merge branch 'master' into mononaut/more-fee-ratings 2023-03-12 16:56:18 +09:00
wiz
b1c6c5cbb6 Merge pull request #3248 from mempool/hunicus/add-yt-pt
Add youtube and peertube links to about page
2023-03-12 16:54:59 +09:00
wiz
7f8f72e9d6 Merge pull request #3306 from mempool/simon/fix-use-same-fee-span-calc
Display same fee span on blocks
2023-03-12 16:52:55 +09:00
wiz
a1ea77ee50 Merge pull request #3311 from mempool/simon/lightning-stats-truncation
Lightning dashboard overflow titles fixes
2023-03-12 16:51:38 +09:00
wiz
19e2d687d0 Use correct BitcoinTV logo 2023-03-12 16:48:23 +09:00
Mononaut
4be8016eb1 Fix repeated new block animation on page navigation 2023-03-12 16:42:58 +09:00
wiz
627c913d5e Merge branch 'master' into simon/fix-use-same-fee-span-calc 2023-03-12 16:17:51 +09:00
wiz
3c5165603b Merge branch 'master' into simon/lightning-stats-truncation 2023-03-12 16:17:22 +09:00
hunicus
513277cef7 Replace peertube logo with bitcointv logo 2023-03-12 16:09:06 +09:00
hunicus
72a9b71901 Add youtube and peertube links to about page 2023-03-12 16:09:06 +09:00
Mononaut
60bef0eeb6 show tx fee ratings for older blocks 2023-03-12 15:54:39 +09:00
softsimon
eeb97bdb81 Merge pull request #3318 from mempool/simon/pull-transifex-2023-03-12
Pull from transifex
2023-03-12 15:45:44 +09:00
softsimon
8407c07b88 Pull from transifex 2023-03-12 15:44:49 +09:00
softsimon
951cf2daf9 Merge pull request #3314 from mempool/nymkappa/fix-node-distance-overflow
Fix some responsive issue on the node component
2023-03-12 14:37:27 +09:00
nymkappa
31cfbf6625 Fix some responsive issue on the node component 2023-03-12 10:16:49 +09:00
wiz
5dc52250e6 Merge pull request #3312 from mempool/mononaut/difficulty-widget-redesign
Fix epoch length in new difficulty widget
2023-03-11 19:49:07 +09:00
wiz
6b544ac179 Merge branch 'master' into mononaut/difficulty-widget-redesign 2023-03-11 19:38:15 +09:00
wiz
2ad9bf57f7 Merge pull request #3288 from mempool/nymkappa/esplora-warning
Log a warn if there are lot of 404 from esplora tx api while updating nodejs backend mempool
2023-03-11 19:36:51 +09:00
Mononaut
e0e97f0d5e Fix epoch length in difficulty widget 2023-03-11 19:32:59 +09:00
softsimon
c72024b4e3 Lightning dashboard overflow titles fixes
fixes #3127
2023-03-11 19:31:49 +09:00
wiz
093469c164 Merge pull request #3303 from mempool/mononaut/batch-address-outspend-lookup
batch address outspend lookups into <50 txids per request
2023-03-11 19:27:50 +09:00
wiz
24d9977919 Merge branch 'master' into nymkappa/esplora-warning 2023-03-11 18:36:22 +09:00
softsimon
7b2ea9c4c8 Merge pull request #3310 from mempool/simon/next-block-lower-case-css-fix
next block lower case css fix
2023-03-11 18:35:08 +09:00
softsimon
6b4650f3cd next block lower case css fix 2023-03-11 18:34:51 +09:00
softsimon
e971846b7e Merge pull request #3309 from mempool/simon/fix-i18n-duplicate-warning
Fixes i18n duplicate warning
2023-03-11 18:33:08 +09:00
softsimon
23ea5d582b Fixes i18n duplicate warning 2023-03-11 18:32:47 +09:00
softsimon
6196860387 Merge pull request #3296 from mempool/nymkappa/order-isp
Sort asn numerically - add few more top 10 isp in warm cache
2023-03-11 18:22:39 +09:00
softsimon
368f858ff2 Merge pull request #3258 from mempool/mononaut/difficulty-widget-redesign
Redesign difficulty adjustment dashboard widget
2023-03-11 18:11:34 +09:00
softsimon
ef0cc9d2db Changing interval to block time 2023-03-11 18:04:06 +09:00
Mononaut
39051e94e3 Redesign difficulty adjustment dashboard widget 2023-03-11 17:53:18 +09:00
softsimon
e1f0bb9901 Merge pull request #3308 from mempool/simon/pull-from-transifex-2023-03-11
Pull from transifex 2023-03-11
2023-03-11 17:26:50 +09:00
softsimon
097eed0baa Pull from transifex 2023-03-11 2023-03-11 17:26:33 +09:00
softsimon
25c7c84705 Merge pull request #3297 from mempool/nymkappa/duplicate-block
Fixes duplicate block in latest block component
2023-03-11 16:09:40 +09:00
wiz
7e873e6637 Merge branch 'master' into nymkappa/order-isp 2023-03-11 15:50:31 +09:00
wiz
36e1777b96 Merge branch 'master' into mononaut/batch-address-outspend-lookup 2023-03-11 15:16:59 +09:00
wiz
35a05a420d Merge branch 'master' into nymkappa/duplicate-block 2023-03-11 15:15:56 +09:00
softsimon
1be5c6ec53 Merge pull request #3307 from mempool/nymkappa/update-regtest-examples
Updated regtest example
2023-03-11 11:37:15 +09:00
softsimon
e4aa3c2091 Merge pull request #3304 from mempool/simon/fix-confirmation-arrow-bug
Fixes arrow position on confirmed blocks
2023-03-11 11:31:30 +09:00
nymkappa
4263977d99 Updated regtest example 2023-03-11 10:52:15 +09:00
softsimon
5bd9be7ab1 Display same fee span on blocks
fixes #3282
2023-03-11 10:14:35 +09:00
softsimon
74c95bfdf5 Merge pull request #3300 from mempool/simon/fix-transaction-expression-changed-error
Fixes changed after checked error in transaction page
2023-03-11 10:01:58 +09:00
softsimon
a1c0a58b9d Merge pull request #3305 from mempool/simon/remove-search-bar-border
Remove search bar focus border
2023-03-11 09:56:27 +09:00
softsimon
d3d67627f3 Remove search bar focus border 2023-03-10 20:19:37 +09:00
softsimon
4a0d9cb66f Fixes arrow position on confirmed blocks
fixes #3294
2023-03-10 18:35:26 +09:00
Mononaut
a2d673f9ed batch address outspend lookups into <50 txids per request 2023-03-10 00:27:22 -06:00
softsimon
8dccaee0b0 Merge pull request #3299 from mempool/mononaut/persist-cache-on-exit
Save cache to disk on SIGTERM/SIGINT
2023-03-10 14:27:00 +09:00
softsimon
ccc413a800 Merge pull request #3301 from mempool/mononaut/flow-diagram-alignment
pixel-perfect flow diagrams (again)
2023-03-10 13:38:56 +09:00
softsimon
4d7e23064c Merge pull request #3298 from mempool/mononaut/missing-fee-rating
fix missing cpfp fee ratings on mobile
2023-03-10 13:28:14 +09:00
Mononaut
a22a62836e pixel-perfect flow diagrams 2023-03-09 22:20:54 -06:00
softsimon
dd01371b61 Fixes changed after checked error in transaction page 2023-03-10 12:37:55 +09:00
Mononaut
46d89ac837 prevent disk cache file write corruption 2023-03-09 20:19:22 -06:00
Mononaut
796566e7ae Save cache to disk on SIGTERM/SIGINT 2023-03-09 19:47:54 -06:00
Mononaut
3f234431fb fix missing fee rating on mobile 2023-03-09 19:31:53 -06:00
nymkappa
778e2f9b64 Fixes #3217 2023-03-10 10:26:30 +09:00
nymkappa
6327ce7c89 Sort asn numerically - add few more top 10 isp in warm cache 2023-03-10 09:21:44 +09:00
softsimon
cec8445223 Merge pull request #3293 from mempool/nymkappa/search-autofocus
Autofocus search input when we load the app for the first time
2023-03-09 19:04:03 +09:00
nymkappa
548a6ea664 Autofocus search input when we load the app for the first time 2023-03-09 18:53:29 +09:00
softsimon
9cef9b67c1 Merge pull request #3291 from mempool/revert-3290-nymkappa/bump-axios
Revert "Bump axios from 0.27.2 -> 1.3.4"
2023-03-09 17:46:19 +09:00
softsimon
42228dc70f Revert "Bump axios from 0.27.2 -> 1.3.4" 2023-03-09 17:46:09 +09:00
nymkappa
63dd9fd09e Log a warn if there are lot of 404 from esplora tx api 2023-03-09 17:45:08 +09:00
softsimon
cd3c1ed82e Merge pull request #3290 from mempool/nymkappa/bump-axios
Bump axios from 0.27.2 -> 1.3.4
2023-03-09 17:35:35 +09:00
nymkappa
304089b3d0 Bump axios from 0.27.2 -> 1.3.4 2023-03-09 17:27:19 +09:00
softsimon
c60903565e Merge pull request #3286 from mempool/simon/some-missing-ln-i18n-strings
Fixing Channels and Capacity i18n strings
2023-03-09 15:30:23 +09:00
softsimon
37e94249df Fixing Channels and Capacity i18n strings 2023-03-09 15:30:04 +09:00
softsimon
d2337ae4e8 Merge pull request #3285 from mempool/wiz/add-old-special-block-events
Add old special block events to app constants
2023-03-09 15:14:11 +09:00
softsimon
7557c47502 Merge pull request #3264 from mempool/mononaut/fix-mining-dashboard-updates
Fix stale mining dashboard data
2023-03-09 14:42:28 +09:00
wiz
a8214bcbbd Add old special block events to app constants 2023-03-09 14:30:55 +09:00
softsimon
fcf51e2af8 Merge pull request #3284 from mempool/simon/pull-transifex-2023-03-09
Pull from transifex
2023-03-09 13:19:40 +09:00
softsimon
c657e622eb Merge pull request #3277 from mempool/i18n/enable-danish-disable-catalan
i18n: Enable Danish, disable Catalan
2023-03-09 13:08:02 +09:00
softsimon
526e46b8e4 Pull from transifex 2023-03-09 11:49:26 +09:00
wiz
ead7a13ff0 i18n: Enable Danish, disable Catalan 2023-03-08 21:14:46 +09:00
Mononaut
c3c0696844 Update hashrate estimate when new blocks arrive 2023-03-08 02:37:19 -06:00
Mononaut
2907054a01 Update pool ranking block count when new blocks arrive 2023-03-08 02:33:16 -06:00
wiz
64408bfd16 Merge pull request #3241 from mempool/simon/fiat-space-fix
Remove fiat plus space
2023-03-08 17:02:19 +09:00
wiz
ca690bf123 Merge branch 'master' into simon/fiat-space-fix 2023-03-08 16:47:36 +09:00
softsimon
0243428fe9 Merge pull request #3262 from mempool/simon/pull-from-transifex-2023-03-08
Pull from transifex 8/3
2023-03-08 16:27:12 +09:00
softsimon
901d32d8f7 Pull from transifex 8/3 2023-03-08 16:26:56 +09:00
wiz
8adacd4a0e ops: Add missing unfurl route in nginx/server-common.conf 2023-03-08 16:14:43 +09:00
wiz
b8a3c15ed2 Merge pull request #3259 from mempool/mononaut/fix-cpfp-memory-bug
Fix memory-intensive getCPFPUnindexedBlocks mysql query
2023-03-08 15:10:31 +09:00
wiz
3e31e68a19 Merge branch 'master' into mononaut/fix-cpfp-memory-bug 2023-03-08 14:31:46 +09:00
wiz
626b395ab7 Merge pull request #3227 from mempool/simon/add-4y
Adding 4 year button to mempool graph
2023-03-08 14:31:39 +09:00
Mononaut
5eae84bb75 Fix memory-intensive getCPFPUnindexedBlocks mysql query 2023-03-07 21:01:54 -06:00
softsimon
c5a01135b3 Merge pull request #3249 from knorrium/fix_typo_in_bulk_config
Fix typo in bulk config variable
2023-03-08 11:05:58 +09:00
Felipe Knorr Kuhn
1a4f3b105e Fix typo in bulk config variable 2023-03-07 12:29:00 -08:00
wiz
ae1e2dcb50 Merge branch 'master' into simon/add-4y 2023-03-07 19:00:27 +09:00
softsimon
da25577ea7 Merge pull request #3244 from mempool/mononaut/fix-ln-rtl
Fix miscellaneous RTL layout bugs
2023-03-07 17:00:11 +09:00
Mononaut
5937e959c3 Fix miscellaneous RTL layout bugs 2023-03-06 20:25:27 -06:00
softsimon
6360913e84 Merge pull request #3243 from mempool/simon/pull-from-transifex-2023-03-07
Pull from transifex
2023-03-07 11:11:21 +09:00
softsimon
fb71136dae Pull from transifex 2023-03-07 11:11:02 +09:00
softsimon
3739e5be0d Merge pull request #3238 from mempool/mononaut/fix-404s
Fix unnecessary cpfp/rbf 404 responses
2023-03-06 18:40:32 +09:00
softsimon
ebfd0b9ddd Merge pull request #3225 from mempool/hunicus/responsive-disclaimer
Make faq disclaimer responsive
2023-03-06 16:50:03 +09:00
softsimon
355acfd338 Merge pull request #3239 from mempool/mononaut/fix-cached-rbf-error
don't cache tx data for rbf replacements
2023-03-06 16:45:36 +09:00
softsimon
5a5ebe8435 Remove fiat plus space
fixes #3240
2023-03-06 16:16:52 +09:00
Mononaut
43b2fe2f9a don't cache tx data for rbf replacements 2023-03-06 00:19:12 -06:00
Mononaut
182cb16695 Fix unnecessary cpfp 404 responses 2023-03-06 00:02:21 -06:00
softsimon
5a09e3099c Merge pull request #3237 from mempool/mononaut/unify-time-components
unify time rendering components
2023-03-06 12:24:00 +09:00
Mononaut
7f78fefb21 revert time localization strings 2023-03-05 21:09:22 -06:00
Mononaut
ac932c641c unify time rendering components 2023-03-05 19:26:32 -06:00
softsimon
4f297f0a7a Merge pull request #3234 from mempool/nymkappa/bugfix/ln-world-map-full-height
Show ln channel world map using 100% height
2023-03-05 19:20:28 +09:00
nymkappa
c28d1c4610 Show ln channel world map using 100% height 2023-03-05 17:43:59 +09:00
softsimon
1ad4ff0683 Merge pull request #3235 from mempool/simon/extract-i18n-clearnet
Update clearnet i18n string
2023-03-05 17:37:32 +09:00
softsimon
c50f5d45e1 Update clearnet i18n string 2023-03-05 17:37:15 +09:00
wiz
226a4e9bde Fix two more strings for "Clearnet Only" 2023-03-05 17:34:30 +09:00
wiz
154e65d470 Fix string for "Clearnet Only" 2023-03-05 17:30:32 +09:00
softsimon
a895d21179 Merge pull request #3231 from mempool/simon/i18n-fixes-extract
I18n extract. Some minor fixes.
2023-03-05 17:08:52 +09:00
softsimon
d5ea2aec25 I18n extract. Some minor fixes. 2023-03-05 17:08:35 +09:00
wiz
07271f56d7 Merge pull request #3230 from mempool/mononaut/heap-monitor
Monitor heap memory usage
2023-03-05 15:47:06 +09:00
wiz
f37946118c Change heap size warning to 80% utilization 2023-03-05 15:45:28 +09:00
softsimon
fdbcef29e5 Merge pull request #3212 from mempool/nymkappa/bugfix/initial-pool-download
Fix initial pool update when db is empty
2023-03-05 15:44:14 +09:00
wiz
9eeaf76369 Merge branch 'master' into mononaut/heap-monitor 2023-03-05 15:35:00 +09:00
wiz
a9a2ff0347 Merge pull request #3215 from mempool/nymkappa/bugfix/price
Handle missing price (show 0)
2023-03-05 15:34:31 +09:00
softsimon
aae61bcb45 Merge pull request #3125 from mempool/nymkappa/feature/update-mining-indexer-log
Update some mining indexer log
2023-03-05 15:19:50 +09:00
Mononaut
43bed7cf56 Monitor heap memory usage 2023-03-04 23:13:55 -06:00
wiz
fca813147d Merge branch 'master' into nymkappa/bugfix/price 2023-03-05 14:13:51 +09:00
nymkappa
62ef1d4439 Fix log typo 2023-03-05 08:27:31 +09:00
nymkappa
ff7c85180d Fix initial pool update when db is empty 2023-03-05 08:27:30 +09:00
nymkappa
001be82f5a Move some notice into info 2023-03-05 08:23:05 +09:00
nymkappa
32a260473a Update some mining indexing logs 2023-03-05 08:23:04 +09:00
nymkappa
2e74d7fa4a Remove mining db stats - replaced by runtime state variable 2023-03-05 08:23:04 +09:00
softsimon
4e39c27c75 Adding 4 year button to mempool graph
fixes #3218
2023-03-04 18:48:16 +09:00
hunicus
7b24b124c2 Use svg component for warning svg 2023-03-04 17:40:39 +09:00
hunicus
82b0844928 Make faq disclaimer more responsive 2023-03-04 17:40:39 +09:00
nymkappa
d483362a9b Handle missing price (show 0) 2023-03-04 10:51:13 +09:00
349 changed files with 37646 additions and 19860 deletions

26
.github/workflows/get_image_digest.yml vendored Normal file
View File

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

View File

@@ -1,13 +1,13 @@
# The Mempool Open Source Project™ [![mempool](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ry4br7/master&style=flat-square)](https://dashboard.cypress.io/projects/ry4br7/runs) # The Mempool Open Source Project™ [![mempool](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ry4br7/master&style=flat-square)](https://dashboard.cypress.io/projects/ry4br7/runs)
https://user-images.githubusercontent.com/232186/222445818-234aa6c9-c233-4c52-b3f0-e32b8232893b.mp4 https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4
<br>
Mempool is the fully-featured mempool visualizer, explorer, and API service running at [mempool.space](https://mempool.space/). Mempool is the fully-featured mempool visualizer, explorer, and API service running at [mempool.space](https://mempool.space/).
It is an open-source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market that is evolving Bitcoin into a multi-layer ecosystem. It is an open-source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market that is evolving Bitcoin into a multi-layer ecosystem.
![mempool](https://mempool.space/resources/screenshots/v2.4.0-dashboard.png)
# Installation Methods # Installation Methods
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server. Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server.

View File

@@ -171,52 +171,58 @@ Helpful link: https://gist.github.com/System-Glitch/cb4e87bf1ae3fec9925725bb3ebe
Run bitcoind on regtest: Run bitcoind on regtest:
``` ```
bitcoind -regtest -rpcport=8332 bitcoind -regtest
``` ```
Create a new wallet, if needed: Create a new wallet, if needed:
``` ```
bitcoin-cli -regtest -rpcport=8332 createwallet test bitcoin-cli -regtest createwallet test
``` ```
Load wallet (this command may take a while if you have lot of UTXOs): Load wallet (this command may take a while if you have lot of UTXOs):
``` ```
bitcoin-cli -regtest -rpcport=8332 loadwallet test bitcoin-cli -regtest loadwallet test
``` ```
Get a new address: Get a new address:
``` ```
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) address=$(bitcoin-cli -regtest getnewaddress)
``` ```
Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min): Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min):
``` ```
bitcoin-cli -regtest -rpcport=8332 generatetoaddress 101 $address bitcoin-cli -regtest generatetoaddress 101 $address
``` ```
Send 0.1 BTC at 5 sat/vB to another address: Send 0.1 BTC at 5 sat/vB to another address:
``` ```
./src/bitcoin-cli -named -regtest -rpcport=8332 sendtoaddress address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) amount=0.1 fee_rate=5 bitcoin-cli -named -regtest sendtoaddress address=$(bitcoin-cli -regtest getnewaddress) amount=0.1 fee_rate=5
``` ```
See more example of `sendtoaddress`: See more example of `sendtoaddress`:
``` ```
./src/bitcoin-cli sendtoaddress # will print the help bitcoin-cli sendtoaddress # will print the help
``` ```
Mini script to generate transactions with random TX fee-rate (between 1 to 100 sat/vB). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other. Mini script to generate random network activity (random TX count with random tx fee-rate). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other.
``` ```
#!/bin/bash #!/bin/bash
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) address=$(bitcoin-cli -regtest getnewaddress)
bitcoin-cli -regtest generatetoaddress 101 $address
for i in {1..1000000} for i in {1..1000000}
do do
./src/bitcoin-cli -regtest -rpcport=8332 -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100) for y in $(seq 1 "$(jot -r 1 1 1000)")
do
bitcoin-cli -regtest -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
done
bitcoin-cli -regtest generatetoaddress 1 $address
sleep 5
done done
``` ```
Generate block at regular interval (every 10 seconds in this example): Generate block at regular interval (every 10 seconds in this example):
``` ```
watch -n 10 "./src/bitcoin-cli -regtest -rpcport=8332 generatetoaddress 1 $address" watch -n 10 "bitcoin-cli -regtest generatetoaddress 1 $address"
``` ```
### Mining pools update ### Mining pools update

View File

@@ -27,13 +27,15 @@
"AUDIT": false, "AUDIT": false,
"ADVANCED_GBT_AUDIT": false, "ADVANCED_GBT_AUDIT": false,
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false "CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool" "PASSWORD": "mempool",
"TIMEOUT": 60000
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@@ -41,13 +43,16 @@
"TLS_ENABLED": true "TLS_ENABLED": true
}, },
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000" "REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"RETRY_UNIX_SOCKET_AFTER": 30000
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool" "PASSWORD": "mempool",
"TIMEOUT": 60000
}, },
"DATABASE": { "DATABASE": {
"ENABLED": true, "ENABLED": true,
@@ -56,7 +61,8 @@
"SOCKET": "/var/run/mysql/mysql.sock", "SOCKET": "/var/run/mysql/mysql.sock",
"DATABASE": "mempool", "DATABASE": "mempool",
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool" "PASSWORD": "mempool",
"TIMEOUT": 180000
}, },
"SYSLOG": { "SYSLOG": {
"ENABLED": true, "ENABLED": true,
@@ -91,7 +97,8 @@
"LND": { "LND": {
"TLS_CERT_PATH": "tls.cert", "TLS_CERT_PATH": "tls.cert",
"MACAROON_PATH": "readonly.macaroon", "MACAROON_PATH": "readonly.macaroon",
"REST_API_URL": "https://localhost:8080" "REST_API_URL": "https://localhost:8080",
"TIMEOUT": 10000
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "lightning-rpc" "SOCKET": "lightning-rpc"
@@ -115,5 +122,9 @@
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1", "LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
"BISQ_URL": "https://bisq.markets/api", "BISQ_URL": "https://bisq.markets/api",
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api" "BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
},
"MEMPOOL_SERVICES": {
"API": "https://mempool.space/api",
"ACCELERATIONS": false
} }
} }

6896
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "mempool-backend", "name": "mempool-backend",
"version": "2.5.0-dev", "version": "2.6.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend", "description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space", "homepage": "https://mempool.space",
@@ -28,41 +28,43 @@
"package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint)", "package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint)",
"start": "node --max-old-space-size=2048 dist/index.js", "start": "node --max-old-space-size=2048 dist/index.js",
"start-production": "node --max-old-space-size=16384 dist/index.js", "start-production": "node --max-old-space-size=16384 dist/index.js",
"reindex-updated-pools": "npm run start-production --update-pools",
"reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks",
"test": "./node_modules/.bin/jest --coverage", "test": "./node_modules/.bin/jest --coverage",
"lint": "./node_modules/.bin/eslint . --ext .ts", "lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.20.12", "@babel/core": "^7.21.3",
"@mempool/electrum-client": "^1.1.7", "@mempool/electrum-client": "1.1.9",
"@types/node": "^16.18.11", "@types/node": "^18.15.3",
"axios": "~0.27.2", "axios": "~0.27.2",
"bitcoinjs-lib": "~6.1.0", "bitcoinjs-lib": "~6.1.0",
"crypto-js": "~4.1.1", "crypto-js": "~4.1.1",
"express": "~4.18.2", "express": "~4.18.2",
"maxmind": "~4.3.8", "maxmind": "~4.3.8",
"mysql2": "~2.3.3", "mysql2": "~3.2.0",
"node-worker-threads-pool": "~1.5.1", "node-worker-threads-pool": "~1.5.1",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",
"typescript": "~4.7.4", "typescript": "~4.7.4",
"ws": "~8.11.0" "ws": "~8.13.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.7", "@babel/core": "^7.21.3",
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.15", "@types/express": "^4.17.15",
"@types/jest": "^29.2.5", "@types/jest": "^29.5.0",
"@types/ws": "~8.5.4", "@types/ws": "~8.5.4",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.48.1", "@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.31.0", "eslint": "^8.36.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.7.0",
"jest": "^29.3.1", "jest": "^29.5.0",
"prettier": "^2.8.2", "prettier": "^2.8.4",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.5",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
} }
} }

View File

@@ -16,7 +16,7 @@
"INITIAL_BLOCKS_AMOUNT": 7, "INITIAL_BLOCKS_AMOUNT": 7,
"MEMPOOL_BLOCKS_AMOUNT": 8, "MEMPOOL_BLOCKS_AMOUNT": 8,
"USE_SECOND_NODE_FOR_MINFEE": 10, "USE_SECOND_NODE_FOR_MINFEE": 10,
"EXTERNAL_ASSETS": 11, "EXTERNAL_ASSETS": [],
"EXTERNAL_MAX_RETRY": 12, "EXTERNAL_MAX_RETRY": 12,
"EXTERNAL_RETRY_INTERVAL": 13, "EXTERNAL_RETRY_INTERVAL": 13,
"USER_AGENT": "__MEMPOOL_USER_AGENT__", "USER_AGENT": "__MEMPOOL_USER_AGENT__",
@@ -24,17 +24,19 @@
"INDEXING_BLOCKS_AMOUNT": 14, "INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__POOLS_JSON_URL__", "POOLS_JSON_URL": "__POOLS_JSON_URL__",
"AUDIT": "__MEMPOOL_AUDIT__", "AUDIT": true,
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__", "ADVANCED_GBT_AUDIT": true,
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__", "ADVANCED_GBT_MEMPOOL": true,
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__", "CPFP_INDEXING": true,
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__" "MAX_BLOCKS_BULK_QUERY": 999,
"DISK_CACHE_BLOCK_INTERVAL": 999
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
"PORT": 15, "PORT": 15,
"USERNAME": "__CORE_RPC_USERNAME__", "USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__" "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -42,13 +44,16 @@
"TLS_ENABLED": true "TLS_ENABLED": true
}, },
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__" "REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"RETRY_UNIX_SOCKET_AFTER": 888
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__", "HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": 17, "PORT": 17,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__", "USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__" "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": 2000
}, },
"DATABASE": { "DATABASE": {
"ENABLED": false, "ENABLED": false,
@@ -57,7 +62,8 @@
"PORT": 18, "PORT": 18,
"DATABASE": "__DATABASE_DATABASE__", "DATABASE": "__DATABASE_DATABASE__",
"USERNAME": "__DATABASE_USERNAME__", "USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__" "PASSWORD": "__DATABASE_PASSWORD__",
"TIMEOUT": 3000
}, },
"SYSLOG": { "SYSLOG": {
"ENABLED": false, "ENABLED": false,
@@ -95,21 +101,26 @@
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__" "BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
}, },
"LIGHTNING": { "LIGHTNING": {
"ENABLED": "__LIGHTNING_ENABLED__", "ENABLED": true,
"BACKEND": "__LIGHTNING_BACKEND__", "BACKEND": "__LIGHTNING_BACKEND__",
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__", "TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
"STATS_REFRESH_INTERVAL": 600, "STATS_REFRESH_INTERVAL": 600,
"GRAPH_REFRESH_INTERVAL": 600, "GRAPH_REFRESH_INTERVAL": 600,
"LOGGER_UPDATE_INTERVAL": 30, "LOGGER_UPDATE_INTERVAL": 30,
"FORENSICS_INTERVAL": 43200, "FORENSICS_INTERVAL": 43200,
"FORENSICS_RATE_LIMIT": "__FORENSICS_RATE_LIMIT__" "FORENSICS_RATE_LIMIT": 1234
}, },
"LND": { "LND": {
"TLS_CERT_PATH": "", "TLS_CERT_PATH": "",
"MACAROON_PATH": "", "MACAROON_PATH": "",
"REST_API_URL": "https://localhost:8080" "REST_API_URL": "https://localhost:8080",
"TIMEOUT": 10000
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__" "SOCKET": "__CLIGHTNING_SOCKET__"
},
"MEMPOOl_SERVICES": {
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": "__MEMPOOL_SERVICES_ACCELERATIONS__"
} }
} }

View File

@@ -14,18 +14,20 @@ describe('Mempool Difficulty Adjustment', () => {
750134, // Current block height 750134, // Current block height
0.6280047707459726, // Previous retarget % (Passed through) 0.6280047707459726, // Previous retarget % (Passed through)
'mainnet', // Network (if testnet, next value is non-zero) 'mainnet', // Network (if testnet, next value is non-zero)
0, // If not testnet, not used 0, // Latest block timestamp in seconds (only used if difficulty already locked in)
], ],
{ // Expected Result { // Expected Result
progressPercent: 9.027777777777777, progressPercent: 9.027777777777777,
difficultyChange: 12.562233927411782, difficultyChange: 13.180707740199772,
estimatedRetargetDate: 1661895424692, estimatedRetargetDate: 1661895424692,
remainingBlocks: 1834, remainingBlocks: 1834,
remainingTime: 977591692, remainingTime: 977591692,
previousRetarget: 0.6280047707459726, previousRetarget: 0.6280047707459726,
previousTime: 1660820820,
nextRetargetHeight: 751968, nextRetargetHeight: 751968,
timeAvg: 533038, timeAvg: 533038,
timeOffset: 0, timeOffset: 0,
expectedBlocks: 161.68833333333333,
}, },
], ],
[ // Vector 2 (testnet) [ // Vector 2 (testnet)
@@ -39,15 +41,40 @@ describe('Mempool Difficulty Adjustment', () => {
], ],
{ // Expected Result is same other than timeOffset { // Expected Result is same other than timeOffset
progressPercent: 9.027777777777777, progressPercent: 9.027777777777777,
difficultyChange: 12.562233927411782, difficultyChange: 13.180707740199772,
estimatedRetargetDate: 1661895424692, estimatedRetargetDate: 1661895424692,
remainingBlocks: 1834, remainingBlocks: 1834,
remainingTime: 977591692, remainingTime: 977591692,
previousTime: 1660820820,
previousRetarget: 0.6280047707459726, previousRetarget: 0.6280047707459726,
nextRetargetHeight: 751968, nextRetargetHeight: 751968,
timeAvg: 533038, timeAvg: 533038,
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only) timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes // If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
expectedBlocks: 161.68833333333333,
},
],
[ // Vector 3 (mainnet lock-in (epoch ending 788255))
[ // Inputs
dt('2023-04-20T09:57:33.000Z'), // Last DA time (in seconds)
dt('2023-05-04T14:54:09.000Z'), // Current time (now) (in seconds)
788255, // Current block height
1.7220298879531821, // Previous retarget % (Passed through)
'mainnet', // Network (if testnet, next value is non-zero)
dt('2023-05-04T14:54:26.000Z'), // Latest block timestamp in seconds
],
{ // Expected Result
progressPercent: 99.95039682539682,
difficultyChange: -1.4512637555574193,
estimatedRetargetDate: 1683212658129,
remainingBlocks: 1,
remainingTime: 609129,
previousRetarget: 1.7220298879531821,
previousTime: 1681984653,
nextRetargetHeight: 788256,
timeAvg: 609129,
timeOffset: 0,
expectedBlocks: 2045.66,
}, },
], ],
] as [[number, number, number, number, string, number], DifficultyAdjustment][]; ] as [[number, number, number, number, string, number], DifficultyAdjustment][];

View File

@@ -42,24 +42,27 @@ describe('Mempool Backend Config', () => {
ADVANCED_GBT_MEMPOOL: false, ADVANCED_GBT_MEMPOOL: false,
CPFP_INDEXING: false, CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0, MAX_BLOCKS_BULK_QUERY: 0,
DISK_CACHE_BLOCK_INTERVAL: 6,
}); });
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000' }); expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null, RETRY_UNIX_SOCKET_AFTER: 30000 });
expect(config.CORE_RPC).toStrictEqual({ expect(config.CORE_RPC).toStrictEqual({
HOST: '127.0.0.1', HOST: '127.0.0.1',
PORT: 8332, PORT: 8332,
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool' PASSWORD: 'mempool',
TIMEOUT: 60000
}); });
expect(config.SECOND_CORE_RPC).toStrictEqual({ expect(config.SECOND_CORE_RPC).toStrictEqual({
HOST: '127.0.0.1', HOST: '127.0.0.1',
PORT: 8332, PORT: 8332,
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool' PASSWORD: 'mempool',
TIMEOUT: 60000
}); });
expect(config.DATABASE).toStrictEqual({ expect(config.DATABASE).toStrictEqual({
@@ -69,7 +72,8 @@ describe('Mempool Backend Config', () => {
PORT: 3306, PORT: 3306,
DATABASE: 'mempool', DATABASE: 'mempool',
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool' PASSWORD: 'mempool',
TIMEOUT: 180000,
}); });
expect(config.SYSLOG).toStrictEqual({ expect(config.SYSLOG).toStrictEqual({
@@ -106,6 +110,18 @@ describe('Mempool Backend Config', () => {
BISQ_URL: 'https://bisq.markets/api', BISQ_URL: 'https://bisq.markets/api',
BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api' BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
}); });
expect(config.MAXMIND).toStrictEqual({
ENABLED: false,
GEOLITE2_CITY: '/usr/local/share/GeoIP/GeoLite2-City.mmdb',
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
});
expect(config.MEMPOOL_SERVICES).toStrictEqual({
API: "",
ACCELERATIONS: false,
});
}); });
}); });
@@ -139,6 +155,98 @@ describe('Mempool Backend Config', () => {
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER); expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER); expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
}); });
}); });
test('should ensure the docker start.sh script has default values', () => {
jest.isolateModules(() => {
const startSh = fs.readFileSync(`${__dirname}/../../../docker/backend/start.sh`, 'utf-8');
const fixture = JSON.parse(fs.readFileSync(`${__dirname}/../__fixtures__/mempool-config.template.json`, 'utf8'));
function parseJson(jsonObj, root?) {
for (const [key, value] of Object.entries(jsonObj)) {
// We have a few cases where we can't follow the pattern
if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
console.log('skipping check for MEMPOOL_HTTP_PORT');
return;
}
switch (typeof value) {
case 'object': {
if (Array.isArray(value)) {
return;
} else {
parseJson(value, key);
}
break;
}
default: {
//The flattened string, i.e, __MEMPOOL_ENABLED__
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
//The string used as the environment variable, i.e, MEMPOOL_ENABLED
const envVarStr = `${root ? root : ''}_${key}`;
//The string used as the default value, to be checked as a regex, i.e, __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=(.*)}
const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
console.log(`looking for ${defaultEntry} in the start.sh script`);
const re = new RegExp(defaultEntry);
expect(startSh).toMatch(re);
//The string that actually replaces the values in the config file
const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json';
console.log(`looking for ${sedStr} in the start.sh script`);
expect(startSh).toContain(sedStr);
break;
}
}
}
}
parseJson(fixture);
});
});
test('should ensure that the mempool-config.json Docker template has all the keys', () => {
jest.isolateModules(() => {
const fixture = JSON.parse(fs.readFileSync(`${__dirname}/../__fixtures__/mempool-config.template.json`, 'utf8'));
const dockerJson = fs.readFileSync(`${__dirname}/../../../docker/backend/mempool-config.json`, 'utf-8');
function parseJson(jsonObj, root?) {
for (const [key, value] of Object.entries(jsonObj)) {
switch (typeof value) {
case 'object': {
if (Array.isArray(value)) {
// numbers, arrays and booleans won't be enclosed by quotes
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
expect(dockerJson).toContain(`"${key}": ${replaceStr}`);
break;
} else {
//Check for top level config keys
expect(dockerJson).toContain(`"${key}"`);
parseJson(value, key);
break;
}
}
case 'string': {
// strings should be enclosed by quotes
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
expect(dockerJson).toContain(`"${key}": "${replaceStr}"`);
break;
}
default: {
// numbers, arrays and booleans won't be enclosed by quotes
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
expect(dockerJson).toContain(`"${key}": ${replaceStr}`);
break;
}
}
};
}
parseJson(fixture);
});
});
}); });

View File

@@ -1,21 +1,25 @@
import config from '../config'; import config from '../config';
import logger from '../logger';
import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
class Audit { class Audit {
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }) auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], fresh: string[], score: number } { : { censored: string[], added: string[], fresh: string[], sigop: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], score: 0 }; return { censored: [], added: [], fresh: [], sigop: [], accelerated: [], score: 0, similarity: 1 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template const added: string[] = []; // present in mined block, not in template
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
const accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse const isCensored = {}; // missing, without excuse
const isDisplaced = {}; const isDisplaced = {};
let displacedWeight = 0; let displacedWeight = 0;
let matchedWeight = 0;
let projectedWeight = 0;
const inBlock = {}; const inBlock = {};
const inTemplate = {}; const inTemplate = {};
@@ -23,6 +27,9 @@ class Audit {
const now = Math.round((Date.now() / 1000)); const now = Math.round((Date.now() / 1000));
for (const tx of transactions) { for (const tx of transactions) {
inBlock[tx.txid] = tx; inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid);
}
} }
// coinbase is always expected // coinbase is always expected
if (transactions[0]) { if (transactions[0]) {
@@ -37,12 +44,19 @@ class Audit {
} else { } else {
isCensored[txid] = true; isCensored[txid] = true;
} }
displacedWeight += mempool[txid].weight; displacedWeight += mempool[txid]?.weight || 0;
} else {
matchedWeight += mempool[txid]?.weight || 0;
} }
projectedWeight += mempool[txid]?.weight || 0;
inTemplate[txid] = true; inTemplate[txid] = true;
} }
displacedWeight += (4000 - transactions[0].weight); if (transactions[0]) {
displacedWeight += (4000 - transactions[0].weight);
projectedWeight += transactions[0].weight;
matchedWeight += transactions[0].weight;
}
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
// these displaced transactions should occupy the first N weight units of the next projected block // these displaced transactions should occupy the first N weight units of the next projected block
@@ -52,19 +66,24 @@ class Audit {
let failures = 0; let failures = 0;
while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) { while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) {
const txid = projectedBlocks[1].transactionIds[index]; const txid = projectedBlocks[1].transactionIds[index];
const fits = (mempool[txid].weight - displacedWeightRemaining) < 4000; const tx = mempool[txid];
const feeMatches = mempool[txid].effectiveFeePerVsize >= lastFeeRate; if (tx) {
if (fits || feeMatches) { const fits = (tx.weight - displacedWeightRemaining) < 4000;
isDisplaced[txid] = true; const feeMatches = tx.effectiveFeePerVsize >= lastFeeRate;
if (fits) { if (fits || feeMatches) {
lastFeeRate = Math.min(lastFeeRate, mempool[txid].effectiveFeePerVsize); isDisplaced[txid] = true;
if (fits) {
lastFeeRate = Math.min(lastFeeRate, tx.effectiveFeePerVsize);
}
if (tx.firstSeen == null || (now - (tx?.firstSeen || 0)) > PROPAGATION_MARGIN) {
displacedWeightRemaining -= tx.weight;
}
failures = 0;
} else {
failures++;
} }
if (mempool[txid].firstSeen == null || (now - (mempool[txid]?.firstSeen || 0)) > PROPAGATION_MARGIN) {
displacedWeightRemaining -= mempool[txid].weight;
}
failures = 0;
} else { } else {
failures++; logger.warn('projected transaction missing from mempool cache');
} }
index++; index++;
} }
@@ -78,17 +97,7 @@ class Audit {
} else { } else {
if (!isDisplaced[tx.txid]) { if (!isDisplaced[tx.txid]) {
added.push(tx.txid); added.push(tx.txid);
} else {
} }
let blockIndex = -1;
let index = -1;
projectedBlocks.forEach((block, bi) => {
const i = block.transactionIds.indexOf(tx.txid);
if (i >= 0) {
blockIndex = bi;
index = i;
}
});
overflowWeight += tx.weight; overflowWeight += tx.weight;
} }
totalWeight += tx.weight; totalWeight += tx.weight;
@@ -101,32 +110,41 @@ class Audit {
index = projectedBlocks[0].transactionIds.length - 1; index = projectedBlocks[0].transactionIds.length - 1;
while (index >= 0) { while (index >= 0) {
const txid = projectedBlocks[0].transactionIds[index]; const txid = projectedBlocks[0].transactionIds[index];
if (overflowWeightRemaining > 0) { const tx = mempool[txid];
if (isCensored[txid]) { if (tx) {
delete isCensored[txid]; if (overflowWeightRemaining > 0) {
} if (isCensored[txid]) {
if (mempool[txid].effectiveFeePerVsize > maxOverflowRate) { delete isCensored[txid];
maxOverflowRate = mempool[txid].effectiveFeePerVsize; }
rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005; if (tx.effectiveFeePerVsize > maxOverflowRate) {
} maxOverflowRate = tx.effectiveFeePerVsize;
} else if (mempool[txid].effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005;
if (isCensored[txid]) { }
delete isCensored[txid]; } else if (tx.effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding
if (isCensored[txid]) {
delete isCensored[txid];
}
} }
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
} else {
logger.warn('projected transaction missing from mempool cache');
} }
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
index--; index--;
} }
const numCensored = Object.keys(isCensored).length; const numCensored = Object.keys(isCensored).length;
const numMatches = matches.length - 1; // adjust for coinbase tx const numMatches = matches.length - 1; // adjust for coinbase tx
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0; const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return { return {
censored: Object.keys(isCensored), censored: Object.keys(isCensored),
added, added,
fresh, fresh,
score sigop: [],
accelerated,
score,
similarity,
}; };
} }
} }

View File

@@ -415,12 +415,38 @@ class BitcoinApi implements AbstractBitcoinApi {
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
} }
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) { if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
const witnessScript = vin.witness[vin.witness.length - 2]; const witnessScript = this.witnessToP2TRScript(vin.witness);
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); if (witnessScript !== null) {
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
} }
} }
/**
* This function must only be called when we know the witness we are parsing
* is a taproot witness.
* @param witness An array of hex strings that represents the witness stack of
* the input.
* @returns null if the witness is not a script spend, and the hex string of
* the script item if it is a script spend.
*/
private witnessToP2TRScript(witness: string[]): string | null {
if (witness.length < 2) return null;
// Note: see BIP341 for parsing details of witness stack
// If there are at least two witness elements, and the first byte of the
// last element is 0x50, this last element is called annex a and
// is removed from the witness stack.
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
// If there are at least two witness elements left, script path spending is used.
// Call the second-to-last stack element s, the script.
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
if (hasAnnex && witness.length < 3) return null;
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript];
}
} }
export default BitcoinApi; export default BitcoinApi;

View File

@@ -7,7 +7,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
port: config.CORE_RPC.PORT, port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME, user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD, pass: config.CORE_RPC.PASSWORD,
timeout: 60000, timeout: config.CORE_RPC.TIMEOUT,
}; };
export default new bitcoin.Client(nodeRpcCredentials); export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -7,7 +7,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
port: config.SECOND_CORE_RPC.PORT, port: config.SECOND_CORE_RPC.PORT,
user: config.SECOND_CORE_RPC.USERNAME, user: config.SECOND_CORE_RPC.USERNAME,
pass: config.SECOND_CORE_RPC.PASSWORD, pass: config.SECOND_CORE_RPC.PASSWORD,
timeout: 60000, timeout: config.SECOND_CORE_RPC.TIMEOUT,
}; };
export default new bitcoin.Client(nodeRpcCredentials); export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -32,8 +32,10 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo) .get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData) .get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress) .get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/rbf', this.getRbfHistory)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
.get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
.get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm) .post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => { .get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try { try {
@@ -94,6 +96,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
@@ -110,7 +113,6 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
@@ -128,8 +130,9 @@ class BitcoinRoutes {
private getInitData(req: Request, res: Response) { private getInitData(req: Request, res: Response) {
try { try {
const result = websocketHandler.getInitData(); const result = websocketHandler.getSerializedInitData();
res.json(result); res.set('Content-Type', 'application/json');
res.send(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }
@@ -208,6 +211,9 @@ class BitcoinRoutes {
bestDescendant: tx.bestDescendant || null, bestDescendant: tx.bestDescendant || null,
descendants: tx.descendants || null, descendants: tx.descendants || null,
effectiveFeePerVsize: tx.effectiveFeePerVsize || null, effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
sigops: tx.sigops,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
}); });
return; return;
} }
@@ -220,18 +226,17 @@ class BitcoinRoutes {
let cpfpInfo; let cpfpInfo;
if (config.DATABASE.ENABLED) { if (config.DATABASE.ENABLED) {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
}
if (cpfpInfo) {
res.json(cpfpInfo);
return;
} else { } else {
res.json({ res.json({
ancestors: [] ancestors: []
}); });
return; return;
} }
if (cpfpInfo) {
res.json(cpfpInfo);
return;
}
} }
res.status(404).send(`Transaction has no CPFP info available.`);
} }
private getBackendInfo(req: Request, res: Response) { private getBackendInfo(req: Request, res: Response) {
@@ -590,10 +595,14 @@ class BitcoinRoutes {
} }
} }
private async getBlockTipHeight(req: Request, res: Response) { private getBlockTipHeight(req: Request, res: Response) {
try { try {
const result = await bitcoinApi.$getBlockHeightTip(); const result = blocks.getCurrentBlockHeight();
res.json(result); if (!result) {
return res.status(503).send(`Service Temporarily Unavailable`);
}
res.setHeader('content-type', 'text/plain');
res.send(result.toString());
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }
@@ -639,8 +648,30 @@ class BitcoinRoutes {
private async getRbfHistory(req: Request, res: Response) { private async getRbfHistory(req: Request, res: Response) {
try { try {
const result = rbfCache.getReplaces(req.params.txId); const replacements = rbfCache.getRbfTree(req.params.txId) || null;
res.json(result || []); const replaces = rbfCache.getReplaces(req.params.txId) || null;
res.json({
replacements,
replaces
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getRbfReplacements(req: Request, res: Response) {
try {
const result = rbfCache.getRbfTrees(false);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getFullRbfReplacements(req: Request, res: Response) {
try {
const result = rbfCache.getRbfTrees(true);
res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }
@@ -652,7 +683,7 @@ class BitcoinRoutes {
if (result) { if (result) {
res.json(result); res.json(result);
} else { } else {
res.status(404).send('not found'); res.status(204).send();
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);

View File

@@ -16,7 +16,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
super(bitcoinClient); super(bitcoinClient);
const electrumConfig = { client: 'mempool-v2', version: '1.4' }; const electrumConfig = { client: 'mempool-v2', version: '1.4' };
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null }; const electrumPersistencePolicy = { retryPeriod: 1000, maxRetry: Number.MAX_SAFE_INTEGER, callback: null };
const electrumCallbacks = { const electrumCallbacks = {
onConnect: (client, versionInfo) => { logger.info(`Connected to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT} (${JSON.stringify(versionInfo)})`); }, onConnect: (client, versionInfo) => { logger.info(`Connected to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT} (${JSON.stringify(versionInfo)})`); },

View File

@@ -3,65 +3,102 @@ import axios, { AxiosRequestConfig } from 'axios';
import http from 'http'; import http from 'http';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
const axiosConnection = axios.create({ const axiosConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true }) httpAgent: new http.Agent({ keepAlive: true, })
}); });
class ElectrsApi implements AbstractBitcoinApi { class ElectrsApi implements AbstractBitcoinApi {
axiosConfig: AxiosRequestConfig = { private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? {
socketPath: config.ESPLORA.UNIX_SOCKET_PATH,
timeout: 10000,
} : {
timeout: 10000,
};
private axiosConfigTcpSocketOnly: AxiosRequestConfig = {
timeout: 10000, timeout: 10000,
}; };
constructor() { } unixSocketRetryTimeout;
activeAxiosConfig;
constructor() {
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
}
fallbackToTcpSocket() {
if (!this.unixSocketRetryTimeout) {
logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`);
// Retry the unix socket after a few seconds
this.unixSocketRetryTimeout = setTimeout(() => {
logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`);
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
this.unixSocketRetryTimeout = undefined;
}, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER);
}
// Use the TCP socket (reach a different esplora instance through nginx)
this.activeAxiosConfig = this.axiosConfigTcpSocketOnly;
}
$queryWrapper<T>(url, responseType = 'json'): Promise<T> {
return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType })
.then((response) => response.data)
.catch((e) => {
if (e?.code === 'ECONNREFUSED') {
this.fallbackToTcpSocket();
// Retry immediately
return axiosConnection.get<T>(url, this.activeAxiosConfig)
.then((response) => response.data)
.catch((e) => {
logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`);
throw e;
});
} else {
throw e;
}
});
}
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> { $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return axiosConnection.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig) return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids');
.then((response) => response.data);
} }
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> { $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
return axiosConnection.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig) return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
.then((response) => response.data);
} }
$getTransactionHex(txId: string): Promise<string> { $getTransactionHex(txId: string): Promise<string> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig) return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
.then((response) => response.data);
} }
$getBlockHeightTip(): Promise<number> { $getBlockHeightTip(): Promise<number> {
return axiosConnection.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig) return this.$queryWrapper<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height');
.then((response) => response.data);
} }
$getBlockHashTip(): Promise<string> { $getBlockHashTip(): Promise<string> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig) return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash');
.then((response) => response.data);
} }
$getTxIdsForBlock(hash: string): Promise<string[]> { $getTxIdsForBlock(hash: string): Promise<string[]> {
return axiosConnection.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig) return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
.then((response) => response.data);
} }
$getBlockHash(height: number): Promise<string> { $getBlockHash(height: number): Promise<string> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig) return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
.then((response) => response.data);
} }
$getBlockHeader(hash: string): Promise<string> { $getBlockHeader(hash: string): Promise<string> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig) return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header');
.then((response) => response.data);
} }
$getBlock(hash: string): Promise<IEsploraApi.Block> { $getBlock(hash: string): Promise<IEsploraApi.Block> {
return axiosConnection.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig) return this.$queryWrapper<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash);
.then((response) => response.data);
} }
$getRawBlock(hash: string): Promise<Buffer> { $getRawBlock(hash: string): Promise<Buffer> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' }) return this.$queryWrapper<any>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer')
.then((response) => { return Buffer.from(response.data); }); .then((response) => { return Buffer.from(response.data); });
} }
@@ -82,13 +119,11 @@ class ElectrsApi implements AbstractBitcoinApi {
} }
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return axiosConnection.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig) return this.$queryWrapper<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout);
.then((response) => response.data);
} }
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> { $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
return axiosConnection.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig) return this.$queryWrapper<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends');
.then((response) => response.data);
} }
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> { async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {

View File

@@ -2,7 +2,7 @@ import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger'; import logger from '../logger';
import memPool from './mempool'; import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo } from '../mempool.interfaces'; import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended } from '../mempool.interfaces';
import { Common } from './common'; import { Common } from './common';
import diskCache from './disk-cache'; import diskCache from './disk-cache';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
@@ -36,6 +36,8 @@ class Blocks {
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = []; private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = [];
private mainLoopTimeout: number = 120000;
constructor() { } constructor() { }
public getBlocks(): BlockExtended[] { public getBlocks(): BlockExtended[] {
@@ -74,6 +76,7 @@ class Blocks {
blockHeight: number, blockHeight: number,
onlyCoinbase: boolean, onlyCoinbase: boolean,
quiet: boolean = false, quiet: boolean = false,
addMempoolData: boolean = false,
): Promise<TransactionExtended[]> { ): Promise<TransactionExtended[]> {
const transactions: TransactionExtended[] = []; const transactions: TransactionExtended[] = [];
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash); const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
@@ -94,14 +97,14 @@ class Blocks {
logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`); logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
} }
try { try {
const tx = await transactionUtils.$getTransactionExtended(txIds[i]); const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData);
transactions.push(tx); transactions.push(tx);
transactionsFetched++; transactionsFetched++;
} catch (e) { } catch (e) {
try { try {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
// Try again with core // Try again with core
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true); const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true, addMempoolData);
transactions.push(tx); transactions.push(tx);
transactionsFetched++; transactionsFetched++;
} else { } else {
@@ -124,11 +127,13 @@ class Blocks {
} }
} }
transactions.forEach((tx) => { if (addMempoolData) {
if (!tx.cpfpChecked) { transactions.forEach((tx) => {
Common.setRelativesAndGetCpfpInfo(tx, mempool); // Child Pay For Parent if (!tx.cpfpChecked) {
} Common.setRelativesAndGetCpfpInfo(tx as MempoolTransactionExtended, mempool); // Child Pay For Parent
}); }
});
}
if (!quiet) { if (!quiet) {
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`); logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
@@ -143,7 +148,10 @@ class Blocks {
* @returns BlockSummary * @returns BlockSummary
*/ */
public summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary { public summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
const stripped = block.tx.map((tx) => { if (Common.isLiquid()) {
block = this.convertLiquidFees(block);
}
const stripped = block.tx.map((tx: IBitcoinApi.VerboseTransaction) => {
return { return {
txid: tx.txid, txid: tx.txid,
vsize: tx.weight / 4, vsize: tx.weight / 4,
@@ -158,6 +166,13 @@ class Blocks {
}; };
} }
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
block.tx.forEach(tx => {
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
});
return block;
}
/** /**
* Return a block with additional data (reward, coinbase, fees...) * Return a block with additional data (reward, coinbase, fees...)
* @param block * @param block
@@ -190,8 +205,15 @@ class Blocks {
extras.segwitTotalWeight = 0; extras.segwitTotalWeight = 0;
} else { } else {
const stats: IBitcoinApi.BlockStats = await bitcoinClient.getBlockStats(block.id); const stats: IBitcoinApi.BlockStats = await bitcoinClient.getBlockStats(block.id);
extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles let feeStats = {
extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(); medianFee: stats.feerate_percentiles[2], // 50th percentiles
feeRange: [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(),
};
if (transactions?.length > 1) {
feeStats = Common.calcEffectiveFeeStatistics(transactions);
}
extras.medianFee = feeStats.medianFee;
extras.feeRange = feeStats.feeRange;
extras.totalFees = stats.totalfee; extras.totalFees = stats.totalfee;
extras.avgFee = stats.avgfee; extras.avgFee = stats.avgfee;
extras.avgFeeRate = stats.avgfeerate; extras.avgFeeRate = stats.avgfeerate;
@@ -287,7 +309,7 @@ class Blocks {
} }
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig); const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
const address = txMinerInfo.vout[0].scriptpubkey_address; const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter((address) => address);
let pools: PoolTag[] = []; let pools: PoolTag[] = [];
if (config.DATABASE.ENABLED === true) { if (config.DATABASE.ENABLED === true) {
@@ -297,11 +319,13 @@ class Blocks {
} }
for (let i = 0; i < pools.length; ++i) { for (let i = 0; i < pools.length; ++i) {
if (address !== undefined) { if (addresses.length) {
const addresses: string[] = typeof pools[i].addresses === 'string' ? const poolAddresses: string[] = typeof pools[i].addresses === 'string' ?
JSON.parse(pools[i].addresses) : pools[i].addresses; JSON.parse(pools[i].addresses) : pools[i].addresses;
if (addresses.indexOf(address) !== -1) { for (let y = 0; y < poolAddresses.length; y++) {
return pools[i]; if (addresses.indexOf(poolAddresses[y]) !== -1) {
return pools[i];
}
} }
} }
@@ -393,12 +417,13 @@ class Blocks {
try { try {
// Get all indexed block hash // Get all indexed block hash
const unindexedBlockHeights = await blocksRepository.$getCPFPUnindexedBlocks(); const unindexedBlockHeights = await blocksRepository.$getCPFPUnindexedBlocks();
logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`);
if (!unindexedBlockHeights?.length) { if (!unindexedBlockHeights?.length) {
return; return;
} }
logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`);
// Logging // Logging
let count = 0; let count = 0;
let countThisRun = 0; let countThisRun = 0;
@@ -509,9 +534,16 @@ class Blocks {
return await BlocksRepository.$validateChain(); return await BlocksRepository.$validateChain();
} }
public async $updateBlocks() { public async $updateBlocks(): Promise<number> {
// warn if this run stalls the main loop for more than 2 minutes
const timer = this.startTimer();
diskCache.lock();
let fastForwarded = false; let fastForwarded = false;
let handledBlocks = 0;
const blockHeightTip = await bitcoinApi.$getBlockHeightTip(); const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
this.updateTimerProgress(timer, 'got block height tip');
if (this.blocks.length === 0) { if (this.blocks.length === 0) {
this.currentBlockHeight = Math.max(blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT, -1); this.currentBlockHeight = Math.max(blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT, -1);
@@ -529,16 +561,21 @@ class Blocks {
if (!this.lastDifficultyAdjustmentTime) { if (!this.lastDifficultyAdjustmentTime) {
const blockchainInfo = await bitcoinClient.getBlockchainInfo(); const blockchainInfo = await bitcoinClient.getBlockchainInfo();
this.updateTimerProgress(timer, 'got blockchain info for initial difficulty adjustment');
if (blockchainInfo.blocks === blockchainInfo.headers) { if (blockchainInfo.blocks === blockchainInfo.headers) {
const heightDiff = blockHeightTip % 2016; const heightDiff = blockHeightTip % 2016;
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff); const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
this.updateTimerProgress(timer, 'got block hash for initial difficulty adjustment');
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash); const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
this.lastDifficultyAdjustmentTime = block.timestamp; this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentDifficulty = block.difficulty; this.currentDifficulty = block.difficulty;
if (blockHeightTip >= 2016) { if (blockHeightTip >= 2016) {
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016); const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash); const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100; this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
logger.debug(`Initial difficulty adjustment data set.`); logger.debug(`Initial difficulty adjustment data set.`);
} }
@@ -548,57 +585,79 @@ class Blocks {
} }
while (this.currentBlockHeight < blockHeightTip) { while (this.currentBlockHeight < blockHeightTip) {
if (this.currentBlockHeight < blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT) { if (this.currentBlockHeight === 0) {
this.currentBlockHeight = blockHeightTip; this.currentBlockHeight = blockHeightTip;
} else { } else {
this.currentBlockHeight++; this.currentBlockHeight++;
logger.debug(`New block found (#${this.currentBlockHeight})!`); logger.debug(`New block found (#${this.currentBlockHeight})!`);
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
await chainTips.updateOrphanedBlocks(); await chainTips.updateOrphanedBlocks();
} }
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight); const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2); const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
const block = BitcoinApi.convertBlock(verboseBlock); const block = BitcoinApi.convertBlock(verboseBlock);
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash); const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false); const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, false, true);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions); if (config.MEMPOOL.BACKEND !== 'esplora') {
// fill in missing transaction fee data from verboseBlock
for (let i = 0; i < transactions.length; i++) {
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
}
}
}
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock); const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
// start async callbacks // start async callbacks
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions)); const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
if (!fastForwarded) { if (!fastForwarded) {
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1); const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
this.updateTimerProgress(timer, `got block by height for ${this.currentBlockHeight}`);
if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) { if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) {
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`); logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`, logger.tags.mining);
// We assume there won't be a reorg with more than 10 block depth // We assume there won't be a reorg with more than 10 block depth
this.updateTimerProgress(timer, `rolling back diverged chain from ${this.currentBlockHeight}`);
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10); await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
await HashratesRepository.$deleteLastEntries(); await HashratesRepository.$deleteLastEntries();
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock.height - 10);
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10); await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`);
for (let i = 10; i >= 0; --i) { for (let i = 10; i >= 0; --i) {
const newBlock = await this.$indexBlock(lastBlock.height - i); const newBlock = await this.$indexBlock(lastBlock.height - i);
this.updateTimerProgress(timer, `reindexed block`);
await this.$getStrippedBlockTransactions(newBlock.id, true, true); await this.$getStrippedBlockTransactions(newBlock.id, true, true);
this.updateTimerProgress(timer, `reindexed block summary`);
if (config.MEMPOOL.CPFP_INDEXING) { if (config.MEMPOOL.CPFP_INDEXING) {
await this.$indexCPFP(newBlock.id, lastBlock.height - i); await this.$indexCPFP(newBlock.id, lastBlock.height - i);
this.updateTimerProgress(timer, `reindexed block cpfp`);
} }
} }
await mining.$indexDifficultyAdjustments(); await mining.$indexDifficultyAdjustments();
await DifficultyAdjustmentsRepository.$deleteLastAdjustment(); await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`); this.updateTimerProgress(timer, `reindexed difficulty adjustments`);
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
indexer.reindex(); indexer.reindex();
} }
await blocksRepository.$saveBlockInDatabase(blockExtended); await blocksRepository.$saveBlockInDatabase(blockExtended);
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
const lastestPriceId = await PricesRepository.$getLatestPriceId(); const lastestPriceId = await PricesRepository.$getLatestPriceId();
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
if (priceUpdater.historyInserted === true && lastestPriceId !== null) { if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
await blocksRepository.$saveBlockPrices([{ await blocksRepository.$saveBlockPrices([{
height: blockExtended.height, height: blockExtended.height,
priceId: lastestPriceId, priceId: lastestPriceId,
}]); }]);
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
} else { } else {
logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining); logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
setTimeout(() => { setTimeout(() => {
indexer.runSingleTask('blocksPrices'); indexer.runSingleTask('blocksPrices');
}, 10000); }, 10000);
@@ -607,9 +666,11 @@ class Blocks {
// Save blocks summary for visualization if it's enabled // Save blocks summary for visualization if it's enabled
if (Common.blocksSummariesIndexingEnabled() === true) { if (Common.blocksSummariesIndexingEnabled() === true) {
await this.$getStrippedBlockTransactions(blockExtended.id, true); await this.$getStrippedBlockTransactions(blockExtended.id, true);
this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`);
} }
if (config.MEMPOOL.CPFP_INDEXING) { if (config.MEMPOOL.CPFP_INDEXING) {
this.$indexCPFP(blockExtended.id, this.currentBlockHeight); this.$saveCpfp(blockExtended.id, this.currentBlockHeight, cpfpSummary);
this.updateTimerProgress(timer, `saved cpfp for ${this.currentBlockHeight}`);
} }
} }
} }
@@ -622,6 +683,7 @@ class Blocks {
difficulty: block.difficulty, difficulty: block.difficulty,
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
}); });
this.updateTimerProgress(timer, `saved difficulty adjustment for ${this.currentBlockHeight}`);
} }
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100; this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
@@ -641,12 +703,44 @@ class Blocks {
if (this.newBlockCallbacks.length) { if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions)); this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
} }
if (!memPool.hasPriority()) { if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
diskCache.$saveCacheToDisk(); diskCache.$saveCacheToDisk();
} }
// wait for pending async callbacks to finish // wait for pending async callbacks to finish
this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
await Promise.all(callbackPromises); await Promise.all(callbackPromises);
this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
handledBlocks++;
}
diskCache.unlock();
this.clearTimer(timer);
return handledBlocks;
}
private startTimer() {
const state: any = {
start: Date.now(),
progress: 'begin $updateBlocks',
timer: null,
};
state.timer = setTimeout(() => {
logger.err(`$updateBlocks stalled at "${state.progress}"`);
}, this.mainLoopTimeout);
return state;
}
private updateTimerProgress(state, msg) {
state.progress = msg;
}
private clearTimer(state) {
if (state.timer) {
clearTimeout(state.timer);
} }
} }
@@ -718,7 +812,7 @@ class Blocks {
// Index the response if needed // Index the response if needed
if (Common.blocksSummariesIndexingEnabled() === true) { if (Common.blocksSummariesIndexingEnabled() === true) {
await BlocksSummariesRepository.$saveSummary({height: block.height, mined: summary}); await BlocksSummariesRepository.$saveTransactions(block.height, block.hash, summary.transactions);
} }
return summary.transactions; return summary.transactions;
@@ -834,11 +928,12 @@ class Blocks {
if (cleanBlock.fee_amt_percentiles === null) { if (cleanBlock.fee_amt_percentiles === null) {
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2); const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
const summary = this.summarizeBlock(block); const summary = this.summarizeBlock(block);
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary }); await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash); cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
} }
if (cleanBlock.fee_amt_percentiles !== null) { if (cleanBlock.fee_amt_percentiles !== null) {
cleanBlock.median_fee_amt = cleanBlock.fee_amt_percentiles[3]; cleanBlock.median_fee_amt = cleanBlock.fee_amt_percentiles[3];
await blocksRepository.$updateFeeAmounts(cleanBlock.hash, cleanBlock.fee_amt_percentiles, cleanBlock.median_fee_amt);
} }
} }
@@ -903,42 +998,20 @@ class Blocks {
public async $indexCPFP(hash: string, height: number): Promise<void> { public async $indexCPFP(hash: string, height: number): Promise<void> {
const block = await bitcoinClient.getBlock(hash, 2); const block = await bitcoinClient.getBlock(hash, 2);
const transactions = block.tx.map(tx => { const transactions = block.tx.map(tx => {
tx.vsize = tx.weight / 4;
tx.fee *= 100_000_000; tx.fee *= 100_000_000;
return tx; return tx;
}); });
const clusters: any[] = []; const summary = Common.calculateCpfp(height, transactions);
let cluster: TransactionStripped[] = []; await this.$saveCpfp(hash, height, summary);
let ancestors: { [txid: string]: boolean } = {};
for (let i = transactions.length - 1; i >= 0; i--) { const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
const tx = transactions[i]; await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
if (!ancestors[tx.txid]) { }
let totalFee = 0;
let totalVSize = 0; public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
cluster.forEach(tx => { const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters);
totalFee += tx?.fee || 0;
totalVSize += tx.vsize;
});
const effectiveFeePerVsize = totalFee / totalVSize;
if (cluster.length > 1) {
clusters.push({
root: cluster[0].txid,
height,
txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }),
effectiveFeePerVsize,
});
}
cluster = [];
ancestors = {};
}
cluster.push(tx);
tx.vin.forEach(vin => {
ancestors[vin.txid] = true;
});
}
const result = await cpfpRepository.$batchSaveClusters(clusters);
if (!result) { if (!result) {
await cpfpRepository.$insertProgressMarker(height); await cpfpRepository.$insertProgressMarker(height);
} }

View File

@@ -1,4 +1,4 @@
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
import config from '../config'; import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net'; import { isIP } from 'net';
@@ -57,32 +57,52 @@ export class Common {
return arr; return arr;
} }
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } { static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[]): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: TransactionExtended } = {}; const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
deleted added
.forEach((deletedTx) => { .forEach((addedTx) => {
const foundMatches = added.find((addedTx) => { const foundMatches = deleted.filter((deletedTx) => {
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx. // The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
return addedTx.fee > deletedTx.fee return addedTx.fee > deletedTx.fee
// The new transaction must pay more fee per kB than the replaced tx. // The new transaction must pay more fee per kB than the replaced tx.
&& addedTx.feePerVsize > deletedTx.feePerVsize && addedTx.adjustedFeePerVsize > deletedTx.adjustedFeePerVsize
// Spends one or more of the same inputs // Spends one or more of the same inputs
&& deletedTx.vin.some((deletedVin) => && deletedTx.vin.some((deletedVin) =>
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout)); addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
}); });
if (foundMatches) { if (foundMatches?.length) {
matches[deletedTx.txid] = foundMatches; matches[addedTx.txid] = foundMatches;
} }
}); });
return matches; return matches;
} }
static findMinedRbfTransactions(minedTransactions: TransactionExtended[], spendMap: Map<string, MempoolTransactionExtended>): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
for (const tx of minedTransactions) {
const replaced: Set<MempoolTransactionExtended> = new Set();
for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[i];
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
if (match && match.txid !== tx.txid) {
replaced.add(match);
}
}
if (replaced.size) {
matches[tx.txid] = { replaced: Array.from(replaced), replacedBy: tx };
}
}
return matches;
}
static stripTransaction(tx: TransactionExtended): TransactionStripped { static stripTransaction(tx: TransactionExtended): TransactionStripped {
return { return {
txid: tx.txid, txid: tx.txid,
fee: tx.fee, fee: tx.fee,
vsize: tx.weight / 4, vsize: tx.weight / 4,
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0), value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
acc: tx.acceleration || undefined,
rate: tx.effectiveFeePerVsize,
}; };
} }
@@ -101,18 +121,18 @@ export class Common {
} }
} }
static setRelativesAndGetCpfpInfo(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): CpfpInfo { static setRelativesAndGetCpfpInfo(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
const parents = this.findAllParents(tx, memPool); const parents = this.findAllParents(tx, memPool);
const lowerFeeParents = parents.filter((parent) => parent.feePerVsize < tx.effectiveFeePerVsize); const lowerFeeParents = parents.filter((parent) => parent.adjustedFeePerVsize < tx.effectiveFeePerVsize);
let totalWeight = tx.weight + lowerFeeParents.reduce((prev, val) => prev + val.weight, 0); let totalWeight = (tx.adjustedVsize * 4) + lowerFeeParents.reduce((prev, val) => prev + (val.adjustedVsize * 4), 0);
let totalFees = tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0); let totalFees = tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
tx.ancestors = parents tx.ancestors = parents
.map((t) => { .map((t) => {
return { return {
txid: t.txid, txid: t.txid,
weight: t.weight, weight: (t.adjustedVsize * 4),
fee: t.fee, fee: t.fee,
}; };
}); });
@@ -133,8 +153,8 @@ export class Common {
} }
private static findAllParents(tx: TransactionExtended, memPool: { [txid: string]: TransactionExtended }): TransactionExtended[] { private static findAllParents(tx: MempoolTransactionExtended, memPool: { [txid: string]: MempoolTransactionExtended }): MempoolTransactionExtended[] {
let parents: TransactionExtended[] = []; let parents: MempoolTransactionExtended[] = [];
tx.vin.forEach((parent) => { tx.vin.forEach((parent) => {
if (parents.find((p) => p.txid === parent.txid)) { if (parents.find((p) => p.txid === parent.txid)) {
return; return;
@@ -142,17 +162,17 @@ export class Common {
const parentTx = memPool[parent.txid]; const parentTx = memPool[parent.txid];
if (parentTx) { if (parentTx) {
if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.feePerVsize) { if (tx.bestDescendant && tx.bestDescendant.fee / (tx.bestDescendant.weight / 4) > parentTx.adjustedFeePerVsize) {
if (parentTx.bestDescendant && parentTx.bestDescendant.fee < tx.fee + tx.bestDescendant.fee) { if (parentTx.bestDescendant && parentTx.bestDescendant.fee < tx.fee + tx.bestDescendant.fee) {
parentTx.bestDescendant = { parentTx.bestDescendant = {
weight: tx.weight + tx.bestDescendant.weight, weight: (tx.adjustedVsize * 4) + tx.bestDescendant.weight,
fee: tx.fee + tx.bestDescendant.fee, fee: tx.fee + tx.bestDescendant.fee,
txid: tx.txid, txid: tx.txid,
}; };
} }
} else if (tx.feePerVsize > parentTx.feePerVsize) { } else if (tx.adjustedFeePerVsize > parentTx.adjustedFeePerVsize) {
parentTx.bestDescendant = { parentTx.bestDescendant = {
weight: tx.weight, weight: (tx.adjustedVsize * 4),
fee: tx.fee, fee: tx.fee,
txid: tx.txid txid: tx.txid
}; };
@@ -164,6 +184,30 @@ export class Common {
return parents; return parents;
} }
// calculates the ratio of matched transactions to projected transactions by weight
static getSimilarity(projectedBlock: MempoolBlockWithTransactions, transactions: TransactionExtended[]): number {
let matchedWeight = 0;
let projectedWeight = 0;
const inBlock = {};
for (const tx of transactions) {
inBlock[tx.txid] = tx;
}
// look for transactions that were expected in the template, but missing from the mined block
for (const tx of projectedBlock.transactions) {
if (inBlock[tx.txid]) {
matchedWeight += tx.vsize * 4;
}
projectedWeight += tx.vsize * 4;
}
projectedWeight += transactions[0].weight;
matchedWeight += transactions[0].weight;
return projectedWeight ? matchedWeight / projectedWeight : 1;
}
static getSqlInterval(interval: string | null): string | null { static getSqlInterval(interval: string | null): string | null {
switch (interval) { switch (interval) {
case '24h': return '1 DAY'; case '24h': return '1 DAY';
@@ -175,6 +219,7 @@ export class Common {
case '1y': return '1 YEAR'; case '1y': return '1 YEAR';
case '2y': return '2 YEAR'; case '2y': return '2 YEAR';
case '3y': return '3 YEAR'; case '3y': return '3 YEAR';
case '4y': return '4 YEAR';
default: return null; default: return null;
} }
} }
@@ -320,4 +365,215 @@ export class Common {
}; };
} }
} }
static calculateCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary {
const clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[] = [];
let cluster: TransactionExtended[] = [];
let ancestors: { [txid: string]: boolean } = {};
const txMap = {};
for (let i = transactions.length - 1; i >= 0; i--) {
const tx = transactions[i];
txMap[tx.txid] = tx;
if (!ancestors[tx.txid]) {
let totalFee = 0;
let totalVSize = 0;
cluster.forEach(tx => {
totalFee += tx?.fee || 0;
totalVSize += (tx.weight / 4);
});
const effectiveFeePerVsize = totalFee / totalVSize;
if (cluster.length > 1) {
clusters.push({
root: cluster[0].txid,
height,
txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
effectiveFeePerVsize,
});
}
cluster.forEach(tx => {
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
});
cluster = [];
ancestors = {};
}
cluster.push(tx);
tx.vin.forEach(vin => {
ancestors[vin.txid] = true;
});
}
return {
transactions,
clusters,
};
}
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string }[]): EffectiveFeeStats {
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
let weightCount = 0;
let medianFee = 0;
let medianWeight = 0;
// calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions
const leftBound = 1995000;
const rightBound = 2005000;
for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) {
const left = weightCount;
const right = weightCount + sortedTxs[i].weight;
if (right > leftBound) {
const weight = Math.min(right, rightBound) - Math.max(left, leftBound);
medianFee += (sortedTxs[i].rate * (weight / 4) );
medianWeight += weight;
}
weightCount += sortedTxs[i].weight;
}
const medianFeeRate = medianWeight ? (medianFee / (medianWeight / 4)) : 0;
// minimum effective fee heuristic:
// lowest of
// a) the 1st percentile of effective fee rates
// b) the minimum effective fee rate in the last 2% of transactions (in block order)
const minFee = Math.min(
Common.getNthPercentile(1, sortedTxs).rate,
transactions.slice(-transactions.length / 50).reduce((min, tx) => { return Math.min(min, tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4))); }, Infinity)
);
// maximum effective fee heuristic:
// highest of
// a) the 99th percentile of effective fee rates
// b) the maximum effective fee rate in the first 2% of transactions (in block order)
const maxFee = Math.max(
Common.getNthPercentile(99, sortedTxs).rate,
transactions.slice(0, transactions.length / 50).reduce((max, tx) => { return Math.max(max, tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4))); }, 0)
);
return {
medianFee: medianFeeRate,
feeRange: [
minFee,
[10,25,50,75,90].map(n => Common.getNthPercentile(n, sortedTxs).rate),
maxFee,
].flat(),
};
}
static getNthPercentile(n: number, sortedDistribution: any[]): any {
return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
}
}
/**
* Class to calculate average fee rates of a list of transactions
* at certain weight percentiles, in a single pass
*
* init with:
* maxWeight - the total weight to measure percentiles relative to (e.g. 4MW for a single block)
* percentileBandWidth - how many weight units to average over for each percentile (as a % of maxWeight)
* percentiles - an array of weight percentiles to compute, in %
*
* then call .processNext(tx) for each transaction, in descending order
*
* retrieve the final results with .getFeeStats()
*/
export class OnlineFeeStatsCalculator {
private maxWeight: number;
private percentiles = [10,25,50,75,90];
private bandWidthPercent = 2;
private bandWidth: number = 0;
private bandIndex = 0;
private leftBound = 0;
private rightBound = 0;
private inBand = false;
private totalBandFee = 0;
private totalBandWeight = 0;
private minBandRate = Infinity;
private maxBandRate = 0;
private feeRange: { avg: number, min: number, max: number }[] = [];
private totalWeight: number = 0;
constructor (maxWeight: number, percentileBandWidth?: number, percentiles?: number[]) {
this.maxWeight = maxWeight;
if (percentiles && percentiles.length) {
this.percentiles = percentiles;
}
if (percentileBandWidth != null) {
this.bandWidthPercent = percentileBandWidth;
}
this.bandWidth = this.maxWeight * (this.bandWidthPercent / 100);
// add min/max percentiles aligned to the ends of the range
this.percentiles.unshift(this.bandWidthPercent / 2);
this.percentiles.push(100 - (this.bandWidthPercent / 2));
this.setNextBounds();
}
processNext(tx: { weight: number, fee: number, effectiveFeePerVsize?: number, feePerVsize?: number, rate?: number, txid: string }): void {
let left = this.totalWeight;
const right = this.totalWeight + tx.weight;
if (!this.inBand && right <= this.leftBound) {
this.totalWeight += tx.weight;
return;
}
while (left < right) {
if (right > this.leftBound) {
this.inBand = true;
const txRate = (tx.rate || tx.effectiveFeePerVsize || tx.feePerVsize || 0);
const weight = Math.min(right, this.rightBound) - Math.max(left, this.leftBound);
this.totalBandFee += (txRate * weight);
this.totalBandWeight += weight;
this.maxBandRate = Math.max(this.maxBandRate, txRate);
this.minBandRate = Math.min(this.minBandRate, txRate);
}
left = Math.min(right, this.rightBound);
if (left >= this.rightBound) {
this.inBand = false;
const avgBandFeeRate = this.totalBandWeight ? (this.totalBandFee / this.totalBandWeight) : 0;
this.feeRange.unshift({ avg: avgBandFeeRate, min: this.minBandRate, max: this.maxBandRate });
this.bandIndex++;
this.setNextBounds();
this.totalBandFee = 0;
this.totalBandWeight = 0;
this.minBandRate = Infinity;
this.maxBandRate = 0;
}
}
this.totalWeight += tx.weight;
}
private setNextBounds(): void {
const nextPercentile = this.percentiles[this.bandIndex];
if (nextPercentile != null) {
this.leftBound = ((nextPercentile / 100) * this.maxWeight) - (this.bandWidth / 2);
this.rightBound = this.leftBound + this.bandWidth;
} else {
this.leftBound = Infinity;
this.rightBound = Infinity;
}
}
getRawFeeStats(): WorkingEffectiveFeeStats {
if (this.totalBandWeight > 0) {
const avgBandFeeRate = this.totalBandWeight ? (this.totalBandFee / this.totalBandWeight) : 0;
this.feeRange.unshift({ avg: avgBandFeeRate, min: this.minBandRate, max: this.maxBandRate });
}
while (this.feeRange.length < this.percentiles.length) {
this.feeRange.unshift({ avg: 0, min: 0, max: 0 });
}
return {
minFee: this.feeRange[0].min,
medianFee: this.feeRange[Math.floor(this.feeRange.length / 2)].avg,
maxFee: this.feeRange[this.feeRange.length - 1].max,
feeRange: this.feeRange.map(f => f.avg),
};
}
getFeeStats(): EffectiveFeeStats {
const stats = this.getRawFeeStats();
stats.feeRange[0] = stats.minFee;
stats.feeRange[stats.feeRange.length - 1] = stats.maxFee;
return stats;
}
} }

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 58; private static currentVersion = 62;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@@ -497,6 +497,7 @@ class DatabaseMigration {
this.uniqueLog(logger.notice, this.blocksTruncatedMessage); this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('DELETE FROM `pools`'); await this.$executeQuery('DELETE FROM `pools`');
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1'); await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
this.uniqueLog(logger.notice, '`pools` table has been truncated`'); this.uniqueLog(logger.notice, '`pools` table has been truncated`');
await this.updateToSchemaVersion(56); await this.updateToSchemaVersion(56);
} }
@@ -510,6 +511,32 @@ class DatabaseMigration {
// We only run some migration queries for this version // We only run some migration queries for this version
await this.updateToSchemaVersion(58); await this.updateToSchemaVersion(58);
} }
if (databaseSchemaVersion < 59 && (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet')) {
// https://github.com/mempool/mempool/issues/3360
await this.$executeQuery(`TRUNCATE prices`);
}
if (databaseSchemaVersion < 60 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(60);
}
if (databaseSchemaVersion < 61 && isBitcoin === true) {
// Break block templates into their own table
if (! await this.$checkIfTableExists('blocks_templates')) {
await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
}
await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
await this.updateToSchemaVersion(61);
}
if (databaseSchemaVersion < 62 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(61);
}
} }
/** /**
@@ -1037,7 +1064,7 @@ class DatabaseMigration {
await this.$executeQuery('DELETE FROM `pools`'); await this.$executeQuery('DELETE FROM `pools`');
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1'); await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`); await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
} }
private async $convertCompactCpfpTables(): Promise<void> { private async $convertCompactCpfpTables(): Promise<void> {
try { try {

View File

@@ -9,9 +9,11 @@ export interface DifficultyAdjustment {
remainingBlocks: number; // Block count remainingBlocks: number; // Block count
remainingTime: number; // Duration of time in ms remainingTime: number; // Duration of time in ms
previousRetarget: number; // Percent: -75 to 300 previousRetarget: number; // Percent: -75 to 300
previousTime: number; // Unix time in ms
nextRetargetHeight: number; // Block Height nextRetargetHeight: number; // Block Height
timeAvg: number; // Duration of time in ms timeAvg: number; // Duration of time in ms
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
expectedBlocks: number; // Block count
} }
export function calcDifficultyAdjustment( export function calcDifficultyAdjustment(
@@ -22,31 +24,29 @@ export function calcDifficultyAdjustment(
network: string, network: string,
latestBlockTimestamp: number, latestBlockTimestamp: number,
): DifficultyAdjustment { ): DifficultyAdjustment {
const ESTIMATE_LAG_BLOCKS = 146; // For first 7.2% of epoch, don't estimate.
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
const BLOCK_SECONDS_TARGET = 600; // Bitcoin mainnet const BLOCK_SECONDS_TARGET = 600; // Bitcoin mainnet
const TESTNET_MAX_BLOCK_SECONDS = 1200; // Bitcoin testnet const TESTNET_MAX_BLOCK_SECONDS = 1200; // Bitcoin testnet
const diffSeconds = nowSeconds - DATime; const diffSeconds = Math.max(0, nowSeconds - DATime);
const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0; const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0;
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100; const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch; const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0; const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
const actualTimespan = (blocksInEpoch === 2015 ? latestBlockTimestamp : nowSeconds) - DATime;
let difficultyChange = 0; let difficultyChange = 0;
let timeAvgSecs = BLOCK_SECONDS_TARGET; let timeAvgSecs = blocksInEpoch ? diffSeconds / blocksInEpoch : BLOCK_SECONDS_TARGET;
// Only calculate the estimate once we have 7.2% of blocks in current epoch
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) { difficultyChange = (BLOCK_SECONDS_TARGET / (actualTimespan / (blocksInEpoch + 1)) - 1) * 100;
timeAvgSecs = diffSeconds / blocksInEpoch; // Max increase is x4 (+300%)
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100; if (difficultyChange > 300) {
// Max increase is x4 (+300%) difficultyChange = 300;
if (difficultyChange > 300) { }
difficultyChange = 300; // Max decrease is /4 (-75%)
} if (difficultyChange < -75) {
// Max decrease is /4 (-75%) difficultyChange = -75;
if (difficultyChange < -75) {
difficultyChange = -75;
}
} }
// Testnet difficulty is set to 1 after 20 minutes of no blocks, // Testnet difficulty is set to 1 after 20 minutes of no blocks,
@@ -74,9 +74,11 @@ export function calcDifficultyAdjustment(
remainingBlocks, remainingBlocks,
remainingTime, remainingTime,
previousRetarget, previousRetarget,
previousTime: DATime,
nextRetargetHeight, nextRetargetHeight,
timeAvg, timeAvg,
timeOffset, timeOffset,
expectedBlocks,
}; };
} }

View File

@@ -7,62 +7,141 @@ import logger from '../logger';
import config from '../config'; import config from '../config';
import { TransactionExtended } from '../mempool.interfaces'; import { TransactionExtended } from '../mempool.interfaces';
import { Common } from './common'; import { Common } from './common';
import rbfCache from './rbf-cache';
class DiskCache { class DiskCache {
private cacheSchemaVersion = 3; private cacheSchemaVersion = 3;
private rbfCacheSchemaVersion = 1;
private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-cache.json';
private static TMP_FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/tmp-cache{number}.json';
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json'; private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json'; private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
private static TMP_RBF_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-rbfcache.json';
private static RBF_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/rbfcache.json';
private static CHUNK_FILES = 25; private static CHUNK_FILES = 25;
private isWritingCache = false; private isWritingCache = false;
private ignoreBlocksCache = false;
constructor() { } private semaphore: { resume: (() => void)[], locks: number } = {
resume: [],
locks: 0,
};
async $saveCacheToDisk(): Promise<void> { constructor() {
if (!cluster.isPrimary) {
return;
}
process.on('SIGINT', (e) => {
this.$saveCacheToDisk(true);
process.exit(0);
});
}
async $saveCacheToDisk(sync: boolean = false): Promise<void> {
if (!cluster.isPrimary) { if (!cluster.isPrimary) {
return; return;
} }
if (this.isWritingCache) { if (this.isWritingCache) {
logger.debug('Saving cache already in progress. Skipping.') logger.debug('Saving cache already in progress. Skipping.');
return; return;
} }
try { try {
logger.debug('Writing mempool and blocks data to disk cache (async)...'); logger.debug(`Writing mempool and blocks data to disk cache (${ sync ? 'sync' : 'async' })...`);
this.isWritingCache = true; this.isWritingCache = true;
const mempool = memPool.getMempool(); const mempool = memPool.getMempool();
const mempoolArray: TransactionExtended[] = []; const mempoolArray: TransactionExtended[] = [];
for (const tx in mempool) { for (const tx in mempool) {
mempoolArray.push(mempool[tx]); if (mempool[tx]) {
mempoolArray.push(mempool[tx]);
}
} }
Common.shuffleArray(mempoolArray); Common.shuffleArray(mempoolArray);
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES); const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({ if (sync) {
cacheSchemaVersion: this.cacheSchemaVersion, fs.writeFileSync(DiskCache.TMP_FILE_NAME, JSON.stringify({
blocks: blocks.getBlocks(), network: config.MEMPOOL.NETWORK,
blockSummaries: blocks.getBlockSummaries(), cacheSchemaVersion: this.cacheSchemaVersion,
mempool: {}, blocks: blocks.getBlocks(),
mempoolArray: mempoolArray.splice(0, chunkSize), blockSummaries: blocks.getBlockSummaries(),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.writeFile(DiskCache.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {}, mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize), mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' }); }), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.writeFileSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
}
fs.renameSync(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.renameSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
} else {
await this.$yield();
await fsPromises.writeFile(DiskCache.TMP_FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await this.$yield();
await fsPromises.writeFile(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
}
await fsPromises.rename(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.rename(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
} }
logger.debug('Mempool and blocks data saved to disk cache'); logger.debug('Mempool and blocks data saved to disk cache');
this.isWritingCache = false; this.isWritingCache = false;
} catch (e) { } catch (e) {
logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e)); logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e));
this.isWritingCache = false; this.isWritingCache = false;
} }
try {
logger.debug('Writing rbf data to disk cache (async)...');
this.isWritingCache = true;
const rbfData = rbfCache.dump();
if (sync) {
fs.writeFileSync(DiskCache.TMP_RBF_FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
rbfCacheSchemaVersion: this.rbfCacheSchemaVersion,
rbf: rbfData,
}), { flag: 'w' });
fs.renameSync(DiskCache.TMP_RBF_FILE_NAME, DiskCache.RBF_FILE_NAME);
} else {
await fsPromises.writeFile(DiskCache.TMP_RBF_FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
rbfCacheSchemaVersion: this.rbfCacheSchemaVersion,
rbf: rbfData,
}), { flag: 'w' });
await fsPromises.rename(DiskCache.TMP_RBF_FILE_NAME, DiskCache.RBF_FILE_NAME);
}
logger.debug('Rbf data saved to disk cache');
this.isWritingCache = false;
} catch (e) {
logger.warn('Error writing rbf data to cache file: ' + (e instanceof Error ? e.message : e));
this.isWritingCache = false;
}
} }
wipeCache() { wipeCache(): void {
logger.notice(`Wipping nodejs backend cache/cache*.json files`); logger.notice(`Wiping nodejs backend cache/cache*.json files`);
try { try {
fs.unlinkSync(DiskCache.FILE_NAME); fs.unlinkSync(DiskCache.FILE_NAME);
} catch (e: any) { } catch (e: any) {
@@ -83,7 +162,19 @@ class DiskCache {
} }
} }
loadMempoolCache() { wipeRbfCache() {
logger.notice(`Wipping nodejs backend cache/rbfcache.json file`);
try {
fs.unlinkSync(DiskCache.RBF_FILE_NAME);
} catch (e: any) {
if (e?.code !== 'ENOENT') {
logger.err(`Cannot wipe cache file ${DiskCache.RBF_FILE_NAME}. Exception ${JSON.stringify(e)}`);
}
}
}
async $loadMempoolCache(): Promise<void> {
if (!fs.existsSync(DiskCache.FILE_NAME)) { if (!fs.existsSync(DiskCache.FILE_NAME)) {
return; return;
} }
@@ -97,6 +188,10 @@ class DiskCache {
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.'); logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
return this.wipeCache(); return this.wipeCache();
} }
if (data.network && data.network !== config.MEMPOOL.NETWORK) {
logger.notice('Disk cache contains data from a different network. Clearing it and skipping the cache loading.');
return this.wipeCache();
}
if (data.mempoolArray) { if (data.mempoolArray) {
for (const tx of data.mempoolArray) { for (const tx of data.mempoolArray) {
@@ -119,16 +214,74 @@ class DiskCache {
} }
} }
} catch (e) { } catch (e) {
logger.info('Error parsing ' + fileName + '. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Error parsing ' + fileName + '. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
} }
} }
memPool.setMempool(data.mempool); await memPool.$setMempool(data.mempool);
blocks.setBlocks(data.blocks); if (!this.ignoreBlocksCache) {
blocks.setBlockSummaries(data.blockSummaries || []); blocks.setBlocks(data.blocks);
blocks.setBlockSummaries(data.blockSummaries || []);
} else {
logger.info('Re-saving cache with empty recent blocks data');
await this.$saveCacheToDisk(true);
}
} catch (e) { } catch (e) {
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
} }
try {
let rbfData: any = {};
const rbfCacheData = fs.readFileSync(DiskCache.RBF_FILE_NAME, 'utf8');
if (rbfCacheData) {
logger.info('Restoring rbf data from disk cache');
rbfData = JSON.parse(rbfCacheData);
if (rbfData.rbfCacheSchemaVersion === undefined || rbfData.rbfCacheSchemaVersion !== this.rbfCacheSchemaVersion) {
logger.notice('Rbf disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
return this.wipeRbfCache();
}
if (rbfData.network && rbfData.network !== config.MEMPOOL.NETWORK) {
logger.notice('Rbf disk cache contains data from a different network. Clearing it and skipping the cache loading.');
return this.wipeRbfCache();
}
}
if (rbfData?.rbf) {
rbfCache.load(rbfData.rbf);
}
} catch (e) {
logger.warn('Failed to parse rbf cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
}
}
private $yield(): Promise<void> {
if (this.semaphore.locks) {
logger.debug('Pause writing mempool and blocks data to disk cache (async)');
return new Promise((resolve) => {
this.semaphore.resume.push(resolve);
});
} else {
return Promise.resolve();
}
}
public lock(): void {
this.semaphore.locks++;
}
public unlock(): void {
this.semaphore.locks = Math.max(0, this.semaphore.locks - 1);
if (!this.semaphore.locks && this.semaphore.resume.length) {
const nextResume = this.semaphore.resume.shift();
if (nextResume) {
logger.debug('Resume writing mempool and blocks data to disk cache (async)');
nextResume();
}
}
}
public setIgnoreBlocksCache(): void {
this.ignoreBlocksCache = true;
} }
} }

View File

@@ -417,24 +417,24 @@ class NodesApi {
if (!ispList[isp1]) { if (!ispList[isp1]) {
ispList[isp1] = { ispList[isp1] = {
id: channel.isp1ID.toString(), ids: [channel.isp1ID],
capacity: 0, capacity: 0,
channels: 0, channels: 0,
nodes: {}, nodes: {},
}; };
} else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) { } else if (ispList[isp1].ids.includes(channel.isp1ID) === false) {
ispList[isp1].id += ',' + channel.isp1ID.toString(); ispList[isp1].ids.push(channel.isp1ID);
} }
if (!ispList[isp2]) { if (!ispList[isp2]) {
ispList[isp2] = { ispList[isp2] = {
id: channel.isp2ID.toString(), ids: [channel.isp2ID],
capacity: 0, capacity: 0,
channels: 0, channels: 0,
nodes: {}, nodes: {},
}; };
} else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) { } else if (ispList[isp2].ids.includes(channel.isp2ID) === false) {
ispList[isp2].id += ',' + channel.isp2ID.toString(); ispList[isp2].ids.push(channel.isp2ID);
} }
ispList[isp1].capacity += channel.capacity; ispList[isp1].capacity += channel.capacity;
@@ -444,11 +444,11 @@ class NodesApi {
ispList[isp2].channels += 1; ispList[isp2].channels += 1;
ispList[isp2].nodes[channel.node2PublicKey] = true; ispList[isp2].nodes[channel.node2PublicKey] = true;
} }
const ispRanking: any[] = []; const ispRanking: any[] = [];
for (const isp of Object.keys(ispList)) { for (const isp of Object.keys(ispList)) {
ispRanking.push([ ispRanking.push([
ispList[isp].id, ispList[isp].ids.sort((a, b) => a - b).join(','),
isp, isp,
ispList[isp].capacity, ispList[isp].capacity,
ispList[isp].channels, ispList[isp].channels,

View File

@@ -4,21 +4,29 @@ import * as fs from 'fs';
import { AbstractLightningApi } from '../lightning-api-abstract-factory'; import { AbstractLightningApi } from '../lightning-api-abstract-factory';
import { ILightningApi } from '../lightning-api.interface'; import { ILightningApi } from '../lightning-api.interface';
import config from '../../../config'; import config from '../../../config';
import logger from '../../../logger';
class LndApi implements AbstractLightningApi { class LndApi implements AbstractLightningApi {
axiosConfig: AxiosRequestConfig = {}; axiosConfig: AxiosRequestConfig = {};
constructor() { constructor() {
if (config.LIGHTNING.ENABLED) { if (!config.LIGHTNING.ENABLED) {
return;
}
try {
this.axiosConfig = { this.axiosConfig = {
headers: { headers: {
'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex') 'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex'),
}, },
httpsAgent: new Agent({ httpsAgent: new Agent({
ca: fs.readFileSync(config.LND.TLS_CERT_PATH) ca: fs.readFileSync(config.LND.TLS_CERT_PATH)
}), }),
timeout: 10000 timeout: config.LND.TIMEOUT
}; };
} catch (e) {
config.LIGHTNING.ENABLED = false;
logger.updateNetwork();
logger.err(`Could not initialize LND Macaroon/TLS Cert. Disabling LIGHTNING. ` + (e instanceof Error ? e.message : e));
} }
} }

View File

@@ -2,7 +2,7 @@ import * as fs from 'fs';
import logger from '../../logger'; import logger from '../../logger';
class Icons { class Icons {
private static FILE_NAME = './icons.json'; private static FILE_NAME = '/elements/asset_registry_db/icons.json';
private iconIds: string[] = []; private iconIds: string[] = [];
private icons: { [assetId: string]: string; } = {}; private icons: { [assetId: string]: string; } = {};

View File

@@ -1,15 +1,19 @@
import logger from '../logger'; import logger from '../logger';
import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces'; import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats } from '../mempool.interfaces';
import { Common } from './common'; import { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config'; import config from '../config';
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
import path from 'path'; import path from 'path';
import mempool from './mempool';
class MempoolBlocks { class MempoolBlocks {
private mempoolBlocks: MempoolBlockWithTransactions[] = []; private mempoolBlocks: MempoolBlockWithTransactions[] = [];
private mempoolBlockDeltas: MempoolBlockDelta[] = []; private mempoolBlockDeltas: MempoolBlockDelta[] = [];
private txSelectionWorker: Worker | null = null; private txSelectionWorker: Worker | null = null;
private nextUid: number = 1;
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
constructor() {} constructor() {}
public getMempoolBlocks(): MempoolBlock[] { public getMempoolBlocks(): MempoolBlock[] {
@@ -33,9 +37,9 @@ class MempoolBlocks {
return this.mempoolBlockDeltas; return this.mempoolBlockDeltas;
} }
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] { public updateMempoolBlocks(memPool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
const latestMempool = memPool; const latestMempool = memPool;
const memPoolArray: TransactionExtended[] = []; const memPoolArray: MempoolTransactionExtended[] = [];
for (const i in latestMempool) { for (const i in latestMempool) {
if (latestMempool.hasOwnProperty(i)) { if (latestMempool.hasOwnProperty(i)) {
memPoolArray.push(latestMempool[i]); memPoolArray.push(latestMempool[i]);
@@ -49,17 +53,24 @@ class MempoolBlocks {
tx.ancestors = []; tx.ancestors = [];
tx.cpfpChecked = false; tx.cpfpChecked = false;
if (!tx.effectiveFeePerVsize) { if (!tx.effectiveFeePerVsize) {
tx.effectiveFeePerVsize = tx.feePerVsize; tx.effectiveFeePerVsize = tx.adjustedFeePerVsize;
} }
}); });
// First sort // First sort
memPoolArray.sort((a, b) => b.feePerVsize - a.feePerVsize); memPoolArray.sort((a, b) => {
if (a.adjustedFeePerVsize === b.adjustedFeePerVsize) {
// tie-break by lexicographic txid order for stability
return a.txid < b.txid ? -1 : 1;
} else {
return b.adjustedFeePerVsize - a.adjustedFeePerVsize;
}
});
// Loop through and traverse all ancestors and sum up all the sizes + fees // Loop through and traverse all ancestors and sum up all the sizes + fees
// Pass down size + fee to all unconfirmed children // Pass down size + fee to all unconfirmed children
let sizes = 0; let sizes = 0;
memPoolArray.forEach((tx, i) => { memPoolArray.forEach((tx) => {
sizes += tx.weight; sizes += tx.weight;
if (sizes > 4000000 * 8) { if (sizes > 4000000 * 8) {
return; return;
@@ -68,13 +79,20 @@ class MempoolBlocks {
}); });
// Final sort, by effective fee // Final sort, by effective fee
memPoolArray.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize); memPoolArray.sort((a, b) => {
if (a.effectiveFeePerVsize === b.effectiveFeePerVsize) {
// tie-break by lexicographic txid order for stability
return a.txid < b.txid ? -1 : 1;
} else {
return b.effectiveFeePerVsize - a.effectiveFeePerVsize;
}
});
const end = new Date().getTime(); const end = new Date().getTime();
const time = end - start; const time = end - start;
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds'); logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
const blocks = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks); const blocks = this.calculateMempoolBlocks(memPoolArray);
if (saveResults) { if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks); const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
@@ -85,26 +103,63 @@ class MempoolBlocks {
return blocks; return blocks;
} }
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] { private calculateMempoolBlocks(transactionsSorted: MempoolTransactionExtended[]): MempoolBlockWithTransactions[] {
const mempoolBlocks: MempoolBlockWithTransactions[] = []; const mempoolBlocks: MempoolBlockWithTransactions[] = [];
let blockWeight = 0; let feeStatsCalculator: OnlineFeeStatsCalculator = new OnlineFeeStatsCalculator(config.MEMPOOL.BLOCK_WEIGHT_UNITS);
let onlineStats = false;
let blockSize = 0; let blockSize = 0;
let transactions: TransactionExtended[] = []; let blockWeight = 0;
transactionsSorted.forEach((tx) => { let blockVsize = 0;
let blockFees = 0;
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
let transactionIds: string[] = [];
let transactions: MempoolTransactionExtended[] = [];
transactionsSorted.forEach((tx, index) => {
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) { || mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
tx.position = {
block: mempoolBlocks.length,
vsize: blockVsize + (tx.vsize / 2),
};
blockWeight += tx.weight; blockWeight += tx.weight;
blockVsize += tx.vsize;
blockSize += tx.size; blockSize += tx.size;
transactions.push(tx); blockFees += tx.fee;
if (blockVsize <= sizeLimit) {
transactions.push(tx);
}
transactionIds.push(tx.txid);
if (onlineStats) {
feeStatsCalculator.processNext(tx);
}
} else { } else {
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length)); mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees));
blockVsize = 0;
tx.position = {
block: mempoolBlocks.length,
vsize: blockVsize + (tx.vsize / 2),
};
if (mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
const stackWeight = transactionsSorted.slice(index).reduce((total, tx) => total + (tx.weight || 0), 0);
if (stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
onlineStats = true;
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5);
feeStatsCalculator.processNext(tx);
}
}
blockVsize += tx.vsize;
blockWeight = tx.weight; blockWeight = tx.weight;
blockSize = tx.size; blockSize = tx.size;
blockFees = tx.fee;
transactionIds = [tx.txid];
transactions = [tx]; transactions = [tx];
} }
}); });
if (transactions.length) { if (transactions.length) {
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length)); const feeStats = onlineStats ? feeStatsCalculator.getRawFeeStats() : undefined;
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees, feeStats));
} }
return mempoolBlocks; return mempoolBlocks;
@@ -115,6 +170,7 @@ class MempoolBlocks {
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionStripped[] = []; let added: TransactionStripped[] = [];
let removed: string[] = []; let removed: string[] = [];
const changed: { txid: string, rate: number | undefined, acc: number | undefined }[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) { if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions; added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) { } else if (!mempoolBlocks[i] && prevBlocks[i]) {
@@ -123,7 +179,7 @@ class MempoolBlocks {
const prevIds = {}; const prevIds = {};
const newIds = {}; const newIds = {};
prevBlocks[i].transactions.forEach(tx => { prevBlocks[i].transactions.forEach(tx => {
prevIds[tx.txid] = true; prevIds[tx.txid] = tx;
}); });
mempoolBlocks[i].transactions.forEach(tx => { mempoolBlocks[i].transactions.forEach(tx => {
newIds[tx.txid] = true; newIds[tx.txid] = true;
@@ -136,30 +192,46 @@ class MempoolBlocks {
mempoolBlocks[i].transactions.forEach(tx => { mempoolBlocks[i].transactions.forEach(tx => {
if (!prevIds[tx.txid]) { if (!prevIds[tx.txid]) {
added.push(tx); added.push(tx);
} else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
} }
}); });
} }
mempoolBlockDeltas.push({ mempoolBlockDeltas.push({
added, added,
removed removed,
changed,
}); });
} }
return mempoolBlockDeltas; return mempoolBlockDeltas;
} }
public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> { public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now();
// reset mempool short ids
this.resetUids();
for (const tx of Object.values(newMempool)) {
this.setUid(tx, true);
}
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
// prepare a stripped down version of the mempool with only the minimum necessary data // prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread // to reduce the overhead of passing this data to the worker thread
const strippedMempool: { [txid: string]: ThreadTransaction } = {}; const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
Object.values(newMempool).forEach(entry => { Object.values(newMempool).forEach(entry => {
strippedMempool[entry.txid] = { if (entry.uid != null) {
txid: entry.txid, strippedMempool.set(entry.uid, {
fee: entry.fee, uid: entry.uid,
weight: entry.weight, fee: entry.fee + (useAccelerations ? (accelerations[entry.txid] || 0) : 0),
feePerVsize: entry.fee / (entry.weight / 4), weight: (entry.adjustedVsize * 4),
effectiveFeePerVsize: entry.fee / (entry.weight / 4), sigops: entry.sigops,
vin: entry.vin.map(v => v.txid), feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
}; effectiveFeePerVsize: entry.effectiveFeePerVsize || entry.adjustedFeePerVsize || entry.feePerVsize,
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
});
}
}); });
// (re)initialize tx selection worker thread // (re)initialize tx selection worker thread
@@ -178,7 +250,7 @@ class MempoolBlocks {
// run the block construction algorithm in a separate thread, and wait for a result // run the block construction algorithm in a separate thread, and wait for a result
let threadErrorListener; let threadErrorListener;
try { try {
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => { const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
threadErrorListener = reject; threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => { this.txSelectionWorker?.once('message', (result): void => {
resolve(result); resolve(result);
@@ -186,102 +258,174 @@ class MempoolBlocks {
this.txSelectionWorker?.once('error', reject); this.txSelectionWorker?.once('error', reject);
}); });
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool }); this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
const { blocks, clusters } = await workerResultPromise; const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
// clean up thread error listener // clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener); this.txSelectionWorker?.removeListener('error', threadErrorListener);
return this.processBlockTemplates(newMempool, blocks, clusters, saveResults); const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, accelerations, saveResults);
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
return processed;
} catch (e) { } catch (e) {
logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
} }
return this.mempoolBlocks; return this.mempoolBlocks;
} }
public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> { public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise<void> {
if (!this.txSelectionWorker) { if (!this.txSelectionWorker) {
// need to reset the worker // need to reset the worker
this.makeBlockTemplates(newMempool, saveResults); await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations);
return; return;
} }
const start = Date.now();
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
const addedAndChanged: MempoolTransactionExtended[] = useAccelerations ? accelerationDelta.map(txid => newMempool[txid]).filter(tx => tx != null).concat(added) : added;
for (const tx of addedAndChanged) {
this.setUid(tx);
}
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
// prepare a stripped down version of the mempool with only the minimum necessary data // prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread // to reduce the overhead of passing this data to the worker thread
const addedStripped: ThreadTransaction[] = added.map(entry => { const addedStripped: CompactThreadTransaction[] = addedAndChanged.filter(entry => entry.uid != null).map(entry => {
return { return {
txid: entry.txid, uid: entry.uid || 0,
fee: entry.fee, fee: entry.fee + (useAccelerations ? (accelerations[entry.txid] || 0) : 0),
weight: entry.weight, weight: (entry.adjustedVsize * 4),
feePerVsize: entry.fee / (entry.weight / 4), sigops: entry.sigops,
effectiveFeePerVsize: entry.fee / (entry.weight / 4), feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
vin: entry.vin.map(v => v.txid), effectiveFeePerVsize: entry.effectiveFeePerVsize || entry.adjustedFeePerVsize || entry.feePerVsize,
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
}; };
}); });
// run the block construction algorithm in a separate thread, and wait for a result // run the block construction algorithm in a separate thread, and wait for a result
let threadErrorListener; let threadErrorListener;
try { try {
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => { const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => {
threadErrorListener = reject; threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => { this.txSelectionWorker?.once('message', (result): void => {
resolve(result); resolve(result);
}); });
this.txSelectionWorker?.once('error', reject); this.txSelectionWorker?.once('error', reject);
}); });
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed }); this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
const { blocks, clusters } = await workerResultPromise; const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise);
this.removeUids(removedUids);
// clean up thread error listener // clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener); this.txSelectionWorker?.removeListener('error', threadErrorListener);
this.processBlockTemplates(newMempool, blocks, clusters, saveResults); this.processBlockTemplates(newMempool, blocks, rates, clusters, accelerations, saveResults);
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
} catch (e) { } catch (e) {
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
} }
} }
private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] { private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, accelerations, saveResults): MempoolBlockWithTransactions[] {
// update this thread's mempool with the results for (const txid of Object.keys(rates)) {
blocks.forEach(block => { if (txid in mempool) {
block.forEach(tx => { mempool[txid].effectiveFeePerVsize = rates[txid];
if (tx.txid in mempool) { }
if (tx.effectiveFeePerVsize != null) { }
mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
}
if (tx.cpfpRoot && tx.cpfpRoot in clusters) {
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
const cluster = clusters[tx.cpfpRoot];
let matched = false;
cluster.forEach(txid => {
if (txid === tx.txid) {
matched = true;
} else {
const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: mempool[txid].weight,
};
if (matched) {
descendants.push(relative);
} else {
ancestors.push(relative);
}
}
});
mempool[tx.txid].ancestors = ancestors;
mempool[tx.txid].descendants = descendants;
mempool[tx.txid].bestDescendant = null;
}
mempool[tx.txid].cpfpChecked = tx.cpfpChecked;
}
});
});
// unpack the condensed blocks into proper mempool blocks let hasBlockStack = blocks.length >= 8;
const mempoolBlocks = blocks.map((transactions, blockIndex) => { let stackWeight;
return this.dataToMempoolBlocks(transactions.map(tx => { let feeStatsCalculator: OnlineFeeStatsCalculator | void;
return mempool[tx.txid] || null; if (hasBlockStack) {
}).filter(tx => !!tx), blockIndex); stackWeight = blocks[blocks.length - 1].reduce((total, tx) => total + (mempool[tx]?.weight || 0), 0);
hasBlockStack = stackWeight > config.MEMPOOL.BLOCK_WEIGHT_UNITS;
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5);
}
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees, feeStats }[] = [];
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block: string[] = blocks[blockIndex];
let txid: string;
let mempoolTx: MempoolTransactionExtended;
let totalSize = 0;
let totalVsize = 0;
let totalWeight = 0;
let totalFees = 0;
const transactions: MempoolTransactionExtended[] = [];
for (let txIndex = 0; txIndex < block.length; txIndex++) {
txid = block[txIndex];
if (txid) {
mempoolTx = mempool[txid];
// save position in projected blocks
mempoolTx.position = {
block: blockIndex,
vsize: totalVsize + (mempoolTx.vsize / 2),
};
mempoolTx.cpfpChecked = true;
mempoolTx.acceleration = accelerations[txid];
// online calculation of stack-of-blocks fee stats
if (hasBlockStack && blockIndex === blocks.length - 1 && feeStatsCalculator) {
feeStatsCalculator.processNext(mempoolTx);
}
totalSize += mempoolTx.size;
totalVsize += mempoolTx.vsize;
totalWeight += mempoolTx.weight;
totalFees += mempoolTx.fee;
if (totalVsize <= sizeLimit) {
transactions.push(mempoolTx);
}
}
}
readyBlocks.push({
transactionIds: block,
transactions,
totalSize,
totalWeight,
totalFees,
feeStats: (hasBlockStack && blockIndex === blocks.length - 1 && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
});
}
for (const cluster of Object.values(clusters)) {
for (const memberTxid of cluster) {
if (memberTxid in mempool) {
const mempoolTx = mempool[memberTxid];
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let matched = false;
cluster.forEach(txid => {
if (txid === memberTxid) {
matched = true;
} else {
const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: (mempool[txid].adjustedVsize * 4),
};
if (matched) {
descendants.push(relative);
} else {
ancestors.push(relative);
}
}
});
mempoolTx.ancestors = ancestors;
mempoolTx.descendants = descendants;
mempoolTx.bestDescendant = null;
}
}
}
const mempoolBlocks = readyBlocks.map((b, index) => {
return this.dataToMempoolBlocks(b.transactionIds, b.transactions, b.totalSize, b.totalWeight, b.totalFees, b.feeStats);
}); });
if (saveResults) { if (saveResults) {
@@ -293,37 +437,77 @@ class MempoolBlocks {
return mempoolBlocks; return mempoolBlocks;
} }
private dataToMempoolBlocks(transactions: TransactionExtended[], blocksIndex: number): MempoolBlockWithTransactions { private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
let totalSize = 0; if (!feeStats) {
let totalWeight = 0; feeStats = Common.calcEffectiveFeeStatistics(transactions);
const fitTransactions: TransactionExtended[] = [];
transactions.forEach(tx => {
totalSize += tx.size;
totalWeight += tx.weight;
if ((totalWeight + tx.weight) <= config.MEMPOOL.BLOCK_WEIGHT_UNITS * 1.2) {
fitTransactions.push(tx);
}
});
let rangeLength = 4;
if (blocksIndex === 0) {
rangeLength = 8;
}
if (transactions.length > 4000) {
rangeLength = 6;
} else if (transactions.length > 10000) {
rangeLength = 8;
} }
return { return {
blockSize: totalSize, blockSize: totalSize,
blockVSize: totalWeight / 4, blockVSize: (totalWeight / 4), // fractional vsize to avoid rounding errors
nTx: transactions.length, nTx: transactionIds.length,
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0), totalFees: totalFees,
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE), medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
feeRange: Common.getFeesInRange(transactions, rangeLength), feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength),
transactionIds: transactions.map((tx) => tx.txid), transactionIds: transactionIds,
transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)), transactions: transactions.map((tx) => Common.stripTransaction(tx)),
}; };
} }
private resetUids(): void {
this.uidMap.clear();
this.nextUid = 1;
}
// use reset=true to overwrite existing uids held by tx objects (required after resetUids)
private setUid(tx: MempoolTransactionExtended, reset = false): number {
let uid = reset ? null : this.getUid(tx);
if (uid == null) {
uid = this.nextUid;
this.nextUid++;
this.uidMap.set(uid, tx.txid);
tx.uid = uid;
return uid;
} else {
return uid;
}
}
private getUid(tx: MempoolTransactionExtended): number | void {
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
return tx.uid;
}
}
private removeUids(uids: number[]): void {
for (const uid of uids) {
this.uidMap.delete(uid);
}
}
private convertResultTxids({ blocks, rates, clusters }: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]>})
: { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }} {
const convertedBlocks: string[][] = blocks.map(block => block.map(uid => {
return this.uidMap.get(uid) || '';
}));
const convertedRates = {};
for (const rateUid of rates.keys()) {
const rateTxid = this.uidMap.get(rateUid);
if (rateTxid) {
convertedRates[rateTxid] = rates.get(rateUid);
}
}
const convertedClusters = {};
for (const rootUid of clusters.keys()) {
const rootTxid = this.uidMap.get(rootUid);
if (rootTxid) {
const members = clusters.get(rootUid)?.map(uid => {
return this.uidMap.get(uid);
});
convertedClusters[rootTxid] = members;
}
}
return { blocks: convertedBlocks, rates: convertedRates, clusters: convertedClusters } as { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }};
}
} }
export default new MempoolBlocks(); export default new MempoolBlocks();

View File

@@ -1,6 +1,6 @@
import config from '../config'; import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory'; import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { TransactionExtended, VbytesPerSecond } from '../mempool.interfaces'; import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
import logger from '../logger'; import logger from '../logger';
import { Common } from './common'; import { Common } from './common';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
@@ -9,19 +9,21 @@ import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
import accelerationApi, { Acceleration } from './services/acceleration';
class Mempool { class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
private static LAZY_DELETE_AFTER_SECONDS = 30;
private inSync: boolean = false; private inSync: boolean = false;
private mempoolCacheDelta: number = -1; private mempoolCacheDelta: number = -1;
private mempoolCache: { [txId: string]: TransactionExtended } = {}; private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private spendMap = new Map<string, MempoolTransactionExtended>();
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: TransactionExtended[]) => void) | undefined; deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined; deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
private accelerations: { [txId: string]: number } = {};
private txPerSecondArray: number[] = []; private txPerSecondArray: number[] = [];
private txPerSecond: number = 0; private txPerSecond: number = 0;
@@ -31,9 +33,14 @@ class Mempool {
private mempoolProtection = 0; private mempoolProtection = 0;
private latestTransactions: any[] = []; private latestTransactions: any[] = [];
private ESPLORA_MISSING_TX_WARNING_THRESHOLD = 100;
private SAMPLE_TIME = 10000; // In ms
private timer = new Date().getTime();
private missingTxCount = 0;
private mainLoopTimeout: number = 120000;
constructor() { constructor() {
setInterval(this.updateTxPerSecond.bind(this), 1000); setInterval(this.updateTxPerSecond.bind(this), 1000);
setInterval(this.deleteExpiredTransactions.bind(this), 20000);
} }
/** /**
@@ -60,28 +67,38 @@ class Mempool {
return this.latestTransactions; return this.latestTransactions;
} }
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; }, public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn; this.mempoolChangedCallback = fn;
} }
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; }, public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
this.asyncMempoolChangedCallback = fn; this.$asyncMempoolChangedCallback = fn;
} }
public getMempool(): { [txid: string]: TransactionExtended } { public getMempool(): { [txid: string]: MempoolTransactionExtended } {
return this.mempoolCache; return this.mempoolCache;
} }
public setMempool(mempoolData: { [txId: string]: TransactionExtended }) { public getSpendMap(): Map<string, MempoolTransactionExtended> {
return this.spendMap;
}
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
this.mempoolCache = mempoolData; this.mempoolCache = mempoolData;
for (const txid of Object.keys(this.mempoolCache)) {
if (this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) {
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
}
}
if (this.mempoolChangedCallback) { if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []); this.mempoolChangedCallback(this.mempoolCache, [], [], []);
} }
if (this.asyncMempoolChangedCallback) { if (this.$asyncMempoolChangedCallback) {
this.asyncMempoolChangedCallback(this.mempoolCache, [], []); await this.$asyncMempoolChangedCallback(this.mempoolCache, [], [], []);
} }
this.addToSpendMap(Object.values(this.mempoolCache));
} }
public async $updateMemPoolInfo() { public async $updateMemPoolInfo() {
@@ -113,25 +130,41 @@ class Mempool {
return txTimes; return txTimes;
} }
public async $updateMempool(): Promise<void> { public async $updateMempool(transactions: string[]): Promise<void> {
logger.debug(`Updating mempool...`); logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes
const timer = this.startTimer();
const start = new Date().getTime(); const start = new Date().getTime();
let hasChange: boolean = false; let hasChange: boolean = false;
const currentMempoolSize = Object.keys(this.mempoolCache).length; const currentMempoolSize = Object.keys(this.mempoolCache).length;
const transactions = await bitcoinApi.$getRawMempool(); this.updateTimerProgress(timer, 'got raw mempool');
const diff = transactions.length - currentMempoolSize; const diff = transactions.length - currentMempoolSize;
const newTransactions: TransactionExtended[] = []; const newTransactions: MempoolTransactionExtended[] = [];
this.mempoolCacheDelta = Math.abs(diff); this.mempoolCacheDelta = Math.abs(diff);
if (!this.inSync) { if (!this.inSync) {
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100); loadingIndicators.setProgress('mempool', currentMempoolSize / transactions.length * 100);
} }
// https://github.com/mempool/mempool/issues/3283
const logEsplora404 = (missingTxCount, threshold, time) => {
const log = `In the past ${time / 1000} seconds, esplora tx API replied ${missingTxCount} times with a 404 error code while updating nodejs backend mempool`;
if (missingTxCount >= threshold) {
logger.warn(log);
} else if (missingTxCount > 0) {
logger.debug(log);
}
};
let loggerTimer = new Date().getTime() / 1000;
for (const txid of transactions) { for (const txid of transactions) {
if (!this.mempoolCache[txid]) { if (!this.mempoolCache[txid]) {
try { try {
const transaction = await transactionUtils.$getTransactionExtended(txid); const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
this.updateTimerProgress(timer, 'fetched new transaction');
this.mempoolCache[txid] = transaction; this.mempoolCache[txid] = transaction;
if (this.inSync) { if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime()); this.txPerSecondArray.push(new Date().getTime());
@@ -142,16 +175,30 @@ class Mempool {
} }
hasChange = true; hasChange = true;
newTransactions.push(transaction); newTransactions.push(transaction);
} catch (e) { } catch (e: any) {
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
this.missingTxCount++;
}
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e)); logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
} }
} }
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if ((new Date().getTime()) - start > Mempool.WEBSOCKET_REFRESH_RATE_MS) { if (elapsedSeconds > 4) {
break; const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
loadingIndicators.setProgress('mempool', progress);
loggerTimer = new Date().getTime() / 1000;
} }
} }
// Reset esplora 404 counter and log a warning if needed
const elapsedTime = new Date().getTime() - this.timer;
if (elapsedTime > this.SAMPLE_TIME) {
logEsplora404(this.missingTxCount, this.ESPLORA_MISSING_TX_WARNING_THRESHOLD, elapsedTime);
this.timer = new Date().getTime();
this.missingTxCount = 0;
}
// Prevent mempool from clear on bitcoind restart by delaying the deletion // Prevent mempool from clear on bitcoind restart by delaying the deletion
if (this.mempoolProtection === 0 if (this.mempoolProtection === 0
&& currentMempoolSize > 20000 && currentMempoolSize > 20000
@@ -166,7 +213,7 @@ class Mempool {
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES); }, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
} }
const deletedTransactions: TransactionExtended[] = []; const deletedTransactions: MempoolTransactionExtended[] = [];
if (this.mempoolProtection !== 1) { if (this.mempoolProtection !== 1) {
this.mempoolProtection = 0; this.mempoolProtection = 0;
@@ -174,13 +221,20 @@ class Mempool {
const transactionsObject = {}; const transactionsObject = {};
transactions.forEach((txId) => transactionsObject[txId] = true); transactions.forEach((txId) => transactionsObject[txId] = true);
// Flag transactions for lazy deletion // Delete evicted transactions from mempool
for (const tx in this.mempoolCache) { for (const tx in this.mempoolCache) {
if (!transactionsObject[tx] && !this.mempoolCache[tx].deleteAfter) { if (!transactionsObject[tx]) {
deletedTransactions.push(this.mempoolCache[tx]); deletedTransactions.push(this.mempoolCache[tx]);
this.mempoolCache[tx].deleteAfter = new Date().getTime() + Mempool.LAZY_DELETE_AFTER_SECONDS * 1000;
} }
} }
for (const tx of deletedTransactions) {
delete this.mempoolCache[tx.txid];
}
}
const accelerationDelta = await this.$updateAccelerations();
if (accelerationDelta.length) {
hasChange = true;
} }
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
@@ -195,24 +249,118 @@ class Mempool {
this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length); this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions); this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
} }
if (this.asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) { if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
await this.asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions); this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
this.updateTimerProgress(timer, 'completed async mempool callback');
} }
const end = new Date().getTime(); const end = new Date().getTime();
const time = end - start; const time = end - start;
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`); logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
this.clearTimer(timer);
} }
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) { public getAccelerations(): { [txid: string]: number } {
return this.accelerations;
}
public async $updateAccelerations(): Promise<string[]> {
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
return [];
}
try {
const newAccelerations = await accelerationApi.$fetchAccelerations();
const changed: string[] = [];
const newAccelerationMap: { [txid: string]: number } = {};
for (const acceleration of newAccelerations) {
newAccelerationMap[acceleration.txid] = acceleration.feeDelta;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid] !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
this.accelerations = newAccelerationMap;
return changed;
} catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return [];
}
}
private startTimer() {
const state: any = {
start: Date.now(),
progress: 'begin $updateMempool',
timer: null,
};
state.timer = setTimeout(() => {
logger.err(`$updateMempool stalled at "${state.progress}"`);
}, this.mainLoopTimeout);
return state;
}
private updateTimerProgress(state, msg) {
state.progress = msg;
}
private clearTimer(state) {
if (state.timer) {
clearTimeout(state.timer);
}
}
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
for (const rbfTransaction in rbfTransactions) { for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction]) { if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
// Store replaced transactions // Store replaced transactions
rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction].txid); rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
// Erase the replaced transactions from the local mempool }
delete this.mempoolCache[rbfTransaction]; }
}
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
for (const rbfTransaction in rbfTransactions) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions
rbfCache.add(rbfTransactions[rbfTransaction].replaced, transactionUtils.extendMempoolTransaction(rbfTransactions[rbfTransaction].replacedBy));
}
}
}
public addToSpendMap(transactions: MempoolTransactionExtended[]): void {
for (const tx of transactions) {
for (const vin of tx.vin) {
this.spendMap.set(`${vin.txid}:${vin.vout}`, tx);
}
}
}
public removeFromSpendMap(transactions: TransactionExtended[]): void {
for (const tx of transactions) {
for (const vin of tx.vin) {
const key = `${vin.txid}:${vin.vout}`;
if (this.spendMap.get(key)?.txid === tx.txid) {
this.spendMap.delete(key);
}
} }
} }
} }
@@ -230,17 +378,6 @@ class Mempool {
} }
} }
private deleteExpiredTransactions() {
const now = new Date().getTime();
for (const tx in this.mempoolCache) {
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
if (lazyDeleteAt && lazyDeleteAt < now) {
delete this.mempoolCache[tx];
rbfCache.evict(tx);
}
}
}
private $getMempoolInfo() { private $getMempoolInfo() {
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) { if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
return Promise.all([ return Promise.all([

View File

@@ -26,7 +26,7 @@ class MiningRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlocksHealth)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore)
@@ -244,15 +244,15 @@ class MiningRoutes {
} }
} }
private async $getHistoricalBlockPrediction(req: Request, res: Response) { private async $getHistoricalBlocksHealth(req: Request, res: Response) {
try { try {
const blockPredictions = await mining.$getBlockPredictionsHistory(req.params.interval); const blocksHealth = await mining.$getBlocksHealthHistory(req.params.interval);
const blockCount = await BlocksAuditsRepository.$getPredictionsCount(); const blockCount = await BlocksAuditsRepository.$getBlocksHealthCount();
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString()); res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockPredictions.map(prediction => [prediction.time, prediction.height, prediction.match_rate])); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }
@@ -263,7 +263,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) { if (!audit) {
res.status(404).send(`This block has not been audited.`); res.status(204).send(`This block has not been audited.`);
return; return;
} }

View File

@@ -20,10 +20,10 @@ class Mining {
public lastWeeklyHashrateIndexingDate: number | null = null; public lastWeeklyHashrateIndexingDate: number | null = null;
/** /**
* Get historical block predictions match rate * Get historical blocks health
*/ */
public async $getBlockPredictionsHistory(interval: string | null = null): Promise<any> { public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
return await BlocksAuditsRepository.$getBlockPredictionsHistory( return await BlocksAuditsRepository.$getBlocksHealthHistory(
this.getTimeRange(interval), this.getTimeRange(interval),
Common.getSqlInterval(interval) Common.getSqlInterval(interval)
); );
@@ -117,7 +117,7 @@ class Mining {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h); poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) { } catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0; poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
} }
return poolsStatistics; return poolsStatistics;
@@ -141,11 +141,14 @@ class Mining {
const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w'); const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w'); const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
const avgHealth = await BlocksRepository.$getAvgBlockHealthPerPoolId(pool.id);
const totalReward = await BlocksRepository.$getTotalRewardForPoolId(pool.id);
let currentEstimatedHashrate = 0; let currentEstimatedHashrate = 0;
try { try {
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h); currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) { } catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
} }
return { return {
@@ -162,6 +165,8 @@ class Mining {
}, },
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h), estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
reportedHashrate: null, reportedHashrate: null,
avgBlockHealth: avgHealth,
totalReward: totalReward,
}; };
} }
@@ -208,7 +213,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000; const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
logger.debug(`Indexing weekly mining pool hashrate`); logger.debug(`Indexing weekly mining pool hashrate`, logger.tags.mining);
loadingIndicators.setProgress('weekly-hashrate-indexing', 0); loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
@@ -245,7 +250,7 @@ class Mining {
}); });
} }
newlyIndexed += hashrates.length; newlyIndexed += hashrates.length / Math.max(1, pools.length);
await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0; hashrates.length = 0;
} }
@@ -256,7 +261,7 @@ class Mining {
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`); logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false); loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
@@ -268,14 +273,14 @@ class Mining {
} }
this.lastWeeklyHashrateIndexingDate = new Date().getUTCDate(); this.lastWeeklyHashrateIndexingDate = new Date().getUTCDate();
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); logger.info(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining);
} else { } else {
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining);
} }
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e; throw e;
} }
} }
@@ -308,7 +313,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000; const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
logger.debug(`Indexing daily network hashrate`); logger.debug(`Indexing daily network hashrate`, logger.tags.mining);
loadingIndicators.setProgress('daily-hashrate-indexing', 0); loadingIndicators.setProgress('daily-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
@@ -346,7 +351,7 @@ class Mining {
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`); logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('daily-hashrate-indexing', progress); loadingIndicators.setProgress('daily-hashrate-indexing', progress);
@@ -373,14 +378,14 @@ class Mining {
this.lastHashrateIndexingDate = new Date().getUTCDate(); this.lastHashrateIndexingDate = new Date().getUTCDate();
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); logger.info(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
} else { } else {
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
} }
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); logger.err(`Daily network hashrate indexing failed. Trying again later. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e; throw e;
} }
} }
@@ -446,13 +451,13 @@ class Mining {
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5) { if (elapsedSeconds > 5) {
const progress = Math.round(totalBlockChecked / blocks.length * 100); const progress = Math.round(totalBlockChecked / blocks.length * 100);
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`); logger.debug(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
} }
} }
if (totalIndexed > 0) { if (totalIndexed > 0) {
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); logger.info(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
} else { } else {
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
} }
@@ -499,7 +504,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) { if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`; logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
} }
logger.debug(logStr); logger.debug(logStr, logger.tags.mining);
await BlocksRepository.$saveBlockPrices(blocksPrices); await BlocksRepository.$saveBlockPrices(blocksPrices);
blocksPrices.length = 0; blocksPrices.length = 0;
} }
@@ -511,7 +516,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) { if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`; logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
} }
logger.debug(logStr); logger.debug(logStr, logger.tags.mining);
await BlocksRepository.$saveBlockPrices(blocksPrices); await BlocksRepository.$saveBlockPrices(blocksPrices);
} }
} catch (e) { } catch (e) {
@@ -552,8 +557,10 @@ class Mining {
currentBlockHeight -= 10000; currentBlockHeight -= 10000;
} }
if (totalIndexed) { if (totalIndexed > 0) {
logger.info(`Indexing missing coinstatsindex data completed`, logger.tags.mining); logger.info(`Indexing missing coinstatsindex data completed. Indexed ${totalIndexed}`, logger.tags.mining);
} else {
logger.debug(`Indexing missing coinstatsindex data completed. Indexed 0.`, logger.tags.mining);
} }
} }
@@ -568,6 +575,7 @@ class Mining {
private getTimeRange(interval: string | null, scale = 1): number { private getTimeRange(interval: string | null, scale = 1): number {
switch (interval) { switch (interval) {
case '4y': return 43200 * scale; // 12h
case '3y': return 43200 * scale; // 12h case '3y': return 43200 * scale; // 12h
case '2y': return 28800 * scale; // 8h case '2y': return 28800 * scale; // 8h
case '1y': return 28800 * scale; // 8h case '1y': return 28800 * scale; // 8h

View File

@@ -41,7 +41,7 @@ class PoolsParser {
public async migratePoolsJson(): Promise<void> { public async migratePoolsJson(): Promise<void> {
// We also need to wipe the backend cache to make sure we don't serve blocks with // We also need to wipe the backend cache to make sure we don't serve blocks with
// the wrong mining pool (usually happen with unknown blocks) // the wrong mining pool (usually happen with unknown blocks)
diskCache.wipeCache(); diskCache.setIgnoreBlocksCache();
await this.$insertUnknownPool(); await this.$insertUnknownPool();
@@ -118,10 +118,6 @@ class PoolsParser {
* @param pool * @param pool
*/ */
private async $deleteBlocksForPool(pool: PoolTag): Promise<void> { private async $deleteBlocksForPool(pool: PoolTag): Promise<void> {
if (config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING === false) {
return;
}
// Get oldest blocks mined by the pool and assume pools-v2.json updates only concern most recent years // Get oldest blocks mined by the pool and assume pools-v2.json updates only concern most recent years
// Ignore early days of Bitcoin as there were no mining pool yet // Ignore early days of Bitcoin as there were no mining pool yet
const [oldestPoolBlock]: any[] = await DB.query(` const [oldestPoolBlock]: any[] = await DB.query(`

View File

@@ -1,65 +1,341 @@
import { TransactionExtended } from "../mempool.interfaces"; import logger from "../logger";
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { Common } from "./common";
interface RbfTransaction extends TransactionStripped {
rbf?: boolean;
mined?: boolean;
}
interface RbfTree {
tx: RbfTransaction;
time: number;
interval?: number;
mined?: boolean;
fullRbf: boolean;
replaces: RbfTree[];
}
class RbfCache { class RbfCache {
private replacedBy: { [txid: string]: string; } = {}; private replacedBy: Map<string, string> = new Map();
private replaces: { [txid: string]: string[] } = {}; private replaces: Map<string, string[]> = new Map();
private txs: { [txid: string]: TransactionExtended } = {}; private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
private expiring: { [txid: string]: Date } = {}; private dirtyTrees: Set<string> = new Set();
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
private txs: Map<string, MempoolTransactionExtended> = new Map();
private expiring: Map<string, number> = new Map();
constructor() { constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 60); setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
} }
public add(replacedTx: TransactionExtended, newTxId: string): void { public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
this.replacedBy[replacedTx.txid] = newTxId; if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
this.txs[replacedTx.txid] = replacedTx; return;
if (!this.replaces[newTxId]) {
this.replaces[newTxId] = [];
} }
this.replaces[newTxId].push(replacedTx.txid);
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
this.txs.set(newTx.txid, newTxExtended);
// maintain rbf trees
let fullRbf = false;
const replacedTrees: RbfTree[] = [];
for (const replacedTxExtended of replaced) {
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid);
if (treeId) {
const tree = this.rbfTrees.get(treeId);
this.rbfTrees.delete(treeId);
if (tree) {
tree.interval = newTime - tree?.time;
replacedTrees.push(tree);
fullRbf = fullRbf || tree.fullRbf;
}
}
} else {
const replacedTime = replacedTxExtended.firstSeen || (Date.now() / 1000);
replacedTrees.push({
tx: replacedTx,
time: replacedTime,
interval: newTime - replacedTime,
fullRbf: !replacedTx.rbf,
replaces: [],
});
fullRbf = fullRbf || !replacedTx.rbf;
this.txs.set(replacedTx.txid, replacedTxExtended);
}
}
const treeId = replacedTrees[0].tx.txid;
const newTree = {
tx: newTx,
time: newTime,
fullRbf,
replaces: replacedTrees
};
this.rbfTrees.set(treeId, newTree);
this.updateTreeMap(treeId, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
this.dirtyTrees.add(treeId);
} }
public getReplacedBy(txId: string): string | undefined { public getReplacedBy(txId: string): string | undefined {
return this.replacedBy[txId]; return this.replacedBy.get(txId);
} }
public getReplaces(txId: string): string[] | undefined { public getReplaces(txId: string): string[] | undefined {
return this.replaces[txId]; return this.replaces.get(txId);
} }
public getTx(txId: string): TransactionExtended | undefined { public getTx(txId: string): MempoolTransactionExtended | undefined {
return this.txs[txId]; return this.txs.get(txId);
}
public getRbfTree(txId: string): RbfTree | void {
return this.rbfTrees.get(this.treeMap.get(txId) || '');
}
// get a paginated list of RbfTrees
// ordered by most recent replacement time
public getRbfTrees(onlyFullRbf: boolean, after?: string): RbfTree[] {
const limit = 25;
const trees: RbfTree[] = [];
const used = new Set<string>();
const replacements: string[][] = Array.from(this.replacedBy).reverse();
const afterTree = after ? this.treeMap.get(after) : null;
let ready = !afterTree;
for (let i = 0; i < replacements.length && trees.length <= limit - 1; i++) {
const txid = replacements[i][1];
const treeId = this.treeMap.get(txid) || '';
if (treeId === afterTree) {
ready = true;
} else if (ready) {
if (!used.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
used.add(treeId);
if (tree && (!onlyFullRbf || tree.fullRbf)) {
trees.push(tree);
}
}
}
}
return trees;
}
// get map of rbf trees that have been updated since the last call
public getRbfChanges(): { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} {
const changes: { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} = {
trees: {},
map: {},
};
this.dirtyTrees.forEach(id => {
const tree = this.rbfTrees.get(id);
if (tree) {
changes.trees[id] = tree;
this.getTransactionsInTree(tree).forEach(tx => {
changes.map[tx.txid] = id;
});
}
});
this.dirtyTrees = new Set();
return changes;
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
}
}
this.evict(txid);
} }
// flag a transaction as removed from the mempool // flag a transaction as removed from the mempool
public evict(txid): void { public evict(txid: string, fast: boolean = false): void {
this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
this.expiring.set(txid, fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400)); // 24 hours
}
} }
private cleanup(): void { private cleanup(): void {
const currentDate = new Date(); const now = Date.now();
for (const txid in this.expiring) { for (const txid of this.expiring.keys()) {
if (this.expiring[txid] < currentDate) { if ((this.expiring.get(txid) || 0) < now) {
delete this.expiring[txid]; this.expiring.delete(txid);
this.remove(txid); this.remove(txid);
} }
} }
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.expiring.size} due to expire`);
} }
// remove a transaction & all previous versions from the cache // remove a transaction & all previous versions from the cache
private remove(txid): void { private remove(txid): void {
// don't remove a transaction while a newer version remains in the mempool // don't remove a transaction if a newer version remains in the mempool
if (this.replaces[txid] && !this.replacedBy[txid]) { if (!this.replacedBy.has(txid)) {
const replaces = this.replaces[txid]; const replaces = this.replaces.get(txid);
delete this.replaces[txid]; this.replaces.delete(txid);
for (const tx of replaces) { this.treeMap.delete(txid);
this.txs.delete(txid);
this.expiring.delete(txid);
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache // recursively remove prior versions from the cache
delete this.replacedBy[tx]; this.replacedBy.delete(tx);
delete this.txs[tx]; // if this is the id of a tree, remove that too
if (this.treeMap.get(tx) === tx) {
this.rbfTrees.delete(tx);
}
this.remove(tx); this.remove(tx);
} }
} }
} }
private updateTreeMap(newId: string, tree: RbfTree): void {
this.treeMap.set(tree.tx.txid, newId);
tree.replaces.forEach(subtree => {
this.updateTreeMap(newId, subtree);
});
}
private getTransactionsInTree(tree: RbfTree, txs: RbfTransaction[] = []): RbfTransaction[] {
txs.push(tree.tx);
tree.replaces.forEach(subtree => {
this.getTransactionsInTree(subtree, txs);
});
return txs;
}
private setTreeMined(tree: RbfTree, txid: string): void {
if (tree.tx.txid === txid) {
tree.tx.mined = true;
} else {
tree.replaces.forEach(subtree => {
this.setTreeMined(subtree, txid);
});
}
}
public dump(): any {
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
return {
txs: Array.from(this.txs.entries()),
trees,
expiring: Array.from(this.expiring.entries()),
};
}
public async load({ txs, trees, expiring }): Promise<void> {
txs.forEach(txEntry => {
this.txs.set(txEntry[0], txEntry[1]);
});
for (const deflatedTree of trees) {
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry[0])) {
this.expiring.set(expiringEntry[0], new Date(expiringEntry[1]).getTime());
}
});
this.cleanup();
}
exportTree(tree: RbfTree, deflated: any = null) {
if (!deflated) {
deflated = {
root: tree.tx.txid,
};
}
deflated[tree.tx.txid] = {
tx: tree.tx.txid,
txMined: tree.tx.mined,
time: tree.time,
interval: tree.interval,
mined: tree.mined,
fullRbf: tree.fullRbf,
replaces: tree.replaces.map(child => child.tx.txid),
};
tree.replaces.forEach(child => {
this.exportTree(child, deflated);
});
return deflated;
}
async importTree(root, txid, deflated, txs: Map<string, MempoolTransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
const treeInfo = deflated[txid];
const replaces: RbfTree[] = [];
// check if any transactions in this tree have already been confirmed
mined = mined || treeInfo.mined;
let exists = mined;
if (!mined) {
try {
const apiTx = await bitcoinApi.$getRawTransaction(txid);
if (apiTx) {
exists = true;
}
if (apiTx?.status?.confirmed) {
mined = true;
treeInfo.txMined = true;
this.evict(txid, true);
}
} catch (e) {
// most transactions do not exist
}
}
// if the root tx is not in the mempool or the blockchain
// evict this tree as soon as possible
if (root === txid && !exists) {
this.evict(txid, true);
}
// recursively reconstruct child trees
for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(root, childId, deflated, txs, mined);
if (replaced) {
this.replacedBy.set(replaced.tx.txid, txid);
replaces.push(replaced);
if (replaced.mined) {
mined = true;
}
}
}
this.replaces.set(txid, replaces.map(t => t.tx.txid));
const tx = txs.get(txid);
if (!tx) {
return;
}
const strippedTx = Common.stripTransaction(tx) as RbfTransaction;
strippedTx.rbf = tx.vin.some((v) => v.sequence < 0xfffffffe);
strippedTx.mined = treeInfo.txMined;
const tree = {
tx: strippedTx,
time: treeInfo.time,
interval: treeInfo.interval,
mined: mined,
fullRbf: treeInfo.fullRbf,
replaces,
};
this.treeMap.set(txid, root);
if (root === txid) {
this.rbfTrees.set(root, tree);
this.dirtyTrees.add(root);
}
return tree;
}
} }
export default new RbfCache(); export default new RbfCache();

View File

@@ -0,0 +1,38 @@
import { query } from '../../utils/axios-query';
import config from '../../config';
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
export interface Acceleration {
txid: string,
feeDelta: number,
}
class AccelerationApi {
public async $fetchAccelerations(): Promise<Acceleration[]> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
const response = await query(`${config.MEMPOOL_SERVICES.API}/accelerations`);
return (response as Acceleration[]) || [];
} else {
return [];
}
}
public async $fetchPools(): Promise<PoolTag[]> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
const response = await query(`${config.MEMPOOL_SERVICES.API}/partners`);
return (response as PoolTag[]) || [];
} else {
return [];
}
}
public async $isAcceleratedBlock(block: BlockExtended): Promise<boolean> {
const pools = await this.$fetchPools();
if (block?.extras?.pool?.id == null) {
return false;
}
return pools.reduce((match, tag) => match || tag.uniqueId === block.extras.pool.id, false);
}
}
export default new AccelerationApi();

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces'; import { TransactionExtended, MempoolTransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
import { IEsploraApi } from './bitcoin/esplora-api.interface'; import { IEsploraApi } from './bitcoin/esplora-api.interface';
import { Common } from './common'; import { Common } from './common';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib';
class TransactionUtils { class TransactionUtils {
constructor() { } constructor() { }
@@ -22,19 +23,27 @@ class TransactionUtils {
} }
/** /**
* @param txId * @param txId
* @param addPrevouts * @param addPrevouts
* @param lazyPrevouts * @param lazyPrevouts
* @param forceCore - See https://github.com/mempool/mempool/issues/2904 * @param forceCore - See https://github.com/mempool/mempool/issues/2904
*/ */
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<TransactionExtended> { public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
let transaction: IEsploraApi.Transaction; let transaction: IEsploraApi.Transaction;
if (forceCore === true) { if (forceCore === true) {
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true); transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
} else { } else {
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts); transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
} }
return this.extendTransaction(transaction); if (addMempoolData || !transaction?.status?.confirmed) {
return this.extendMempoolTransaction(transaction);
} else {
return this.extendTransaction(transaction);
}
}
public async $getMempoolTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended> {
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
} }
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended { private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
@@ -50,8 +59,32 @@ class TransactionUtils {
feePerVsize: feePerVbytes, feePerVsize: feePerVbytes,
effectiveFeePerVsize: feePerVbytes, effectiveFeePerVsize: feePerVbytes,
}, transaction); }, transaction);
if (!transaction.status.confirmed) { if (!transaction?.status?.confirmed && !transactionExtended.firstSeen) {
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000)); transactionExtended.firstSeen = Math.round((Date.now() / 1000));
}
return transactionExtended;
}
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
const vsize = Math.ceil(transaction.weight / 4);
const fractionalVsize = (transaction.weight / 4);
const sigops = this.countSigops(transaction);
// https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
(transaction.fee || 0) / fractionalVsize);
const adjustedFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1,
(transaction.fee || 0) / adjustedVsize);
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
vsize: Math.round(transaction.weight / 4),
adjustedVsize,
sigops,
feePerVsize: feePerVbytes,
adjustedFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: adjustedFeePerVsize,
});
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
} }
return transactionExtended; return transactionExtended;
} }
@@ -63,6 +96,64 @@ class TransactionUtils {
} }
return str; return str;
} }
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
let sigops = 0;
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
// count OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY
if (isRawScript) {
// in scriptPubKey or scriptSig, always worth 20
sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
} else {
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
const matches = script.matchAll(/(?:OP_(\d+))? OP_CHECKMULTISIG/g);
for (const match of matches) {
const n = parseInt(match[1]);
if (Number.isInteger(n)) {
sigops += n;
} else {
sigops += 20;
}
}
}
return witness ? sigops : (sigops * 4);
}
public countSigops(transaction: IEsploraApi.Transaction): number {
let sigops = 0;
for (const input of transaction.vin) {
if (input.scriptsig_asm) {
sigops += this.countScriptSigops(input.scriptsig_asm, true);
}
if (input.prevout) {
switch (true) {
case input.prevout.scriptpubkey_type === 'p2sh' && input.witness?.length === 2 && input.scriptsig && input.scriptsig.startsWith('160014'):
case input.prevout.scriptpubkey_type === 'v0_p2wpkh':
sigops += 1;
break;
case input.prevout?.scriptpubkey_type === 'p2sh' && input.witness?.length && input.scriptsig && input.scriptsig.startsWith('220020'):
case input.prevout.scriptpubkey_type === 'v0_p2wsh':
if (input.witness?.length) {
sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true);
}
break;
}
}
}
for (const output of transaction.vout) {
if (output.scriptpubkey_asm) {
sigops += this.countScriptSigops(output.scriptpubkey_asm, true);
}
}
return sigops;
}
} }
export default new TransactionUtils(); export default new TransactionUtils();

View File

@@ -1,11 +1,10 @@
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { ThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces'; import { CompactThreadTransaction, AuditTransaction } from '../mempool.interfaces';
import { PairingHeap } from '../utils/pairing-heap'; import { PairingHeap } from '../utils/pairing-heap';
import { Common } from './common';
import { parentPort } from 'worker_threads'; import { parentPort } from 'worker_threads';
let mempool: { [txid: string]: ThreadTransaction } = {}; let mempool: Map<number, CompactThreadTransaction> = new Map();
if (parentPort) { if (parentPort) {
parentPort.on('message', (params) => { parentPort.on('message', (params) => {
@@ -13,18 +12,18 @@ if (parentPort) {
mempool = params.mempool; mempool = params.mempool;
} else if (params.type === 'update') { } else if (params.type === 'update') {
params.added.forEach(tx => { params.added.forEach(tx => {
mempool[tx.txid] = tx; mempool.set(tx.uid, tx);
}); });
params.removed.forEach(txid => { params.removed.forEach(uid => {
delete mempool[txid]; mempool.delete(uid);
}); });
} }
const { blocks, clusters } = makeBlockTemplates(mempool); const { blocks, rates, clusters } = makeBlockTemplates(mempool);
// return the result to main thread. // return the result to main thread.
if (parentPort) { if (parentPort) {
parentPort.postMessage({ blocks, clusters }); parentPort.postMessage({ blocks, rates, clusters });
} }
}); });
} }
@@ -33,35 +32,36 @@ if (parentPort) {
* Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core * Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp) * (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/ */
function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction }) function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
: { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } } { : { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> } {
const start = Date.now(); const start = Date.now();
const auditPool: { [txid: string]: AuditTransaction } = {}; const auditPool: Map<number, AuditTransaction> = new Map();
const mempoolArray: AuditTransaction[] = []; const mempoolArray: AuditTransaction[] = [];
const restOfArray: ThreadTransaction[] = []; const cpfpClusters: Map<number, number[]> = new Map();
const cpfpClusters: { [root: string]: string[] } = {};
// grab the top feerate txs up to maxWeight mempool.forEach(tx => {
Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => { tx.dirty = false;
// initializing everything up front helps V8 optimize property access later // initializing everything up front helps V8 optimize property access later
auditPool[tx.txid] = { auditPool.set(tx.uid, {
txid: tx.txid, uid: tx.uid,
fee: tx.fee, fee: tx.fee,
weight: tx.weight, weight: tx.weight,
feePerVsize: tx.feePerVsize, feePerVsize: tx.feePerVsize,
effectiveFeePerVsize: tx.feePerVsize, effectiveFeePerVsize: tx.feePerVsize,
vin: tx.vin, sigops: tx.sigops,
inputs: tx.inputs || [],
relativesSet: false, relativesSet: false,
ancestorMap: new Map<string, AuditTransaction>(), ancestorMap: new Map<number, AuditTransaction>(),
children: new Set<AuditTransaction>(), children: new Set<AuditTransaction>(),
ancestorFee: 0, ancestorFee: 0,
ancestorWeight: 0, ancestorWeight: 0,
ancestorSigops: 0,
score: 0, score: 0,
used: false, used: false,
modified: false, modified: false,
modifiedNode: null, modifiedNode: null,
}; });
mempoolArray.push(auditPool[tx.txid]); mempoolArray.push(auditPool.get(tx.uid) as AuditTransaction);
}); });
// Build relatives graph & calculate ancestor scores // Build relatives graph & calculate ancestor scores
@@ -72,15 +72,29 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
} }
// Sort by descending ancestor score // Sort by descending ancestor score
mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0)); mempoolArray.sort((a, b) => {
if (b.score === a.score) {
// tie-break by uid for stability
return a.uid < b.uid ? -1 : 1;
} else {
return (b.score || 0) - (a.score || 0);
}
});
// Build blocks by greedily choosing the highest feerate package // Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score) // (i.e. the package rooted in the transaction with the best ancestor score)
const blocks: ThreadTransaction[][] = []; const blocks: number[][] = [];
let blockWeight = 4000; let blockWeight = 4000;
let blockSize = 0; let blockSigops = 0;
let transactions: AuditTransaction[] = []; let transactions: AuditTransaction[] = [];
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0)); const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
if (a.score === b.score) {
// tie-break by uid for stability
return a.uid > b.uid;
} else {
return (a.score || 0) > (b.score || 0);
}
});
let overflow: AuditTransaction[] = []; let overflow: AuditTransaction[] = [];
let failures = 0; let failures = 0;
let top = 0; let top = 0;
@@ -107,30 +121,36 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
if (nextTx && !nextTx?.used) { if (nextTx && !nextTx?.used) {
// Check if the package fits into this block // Check if the package fits into this block
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) { if (blocks.length >= 7 || ((blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) && (blockSigops + nextTx.ancestorSigops <= 80000))) {
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values()); const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count) // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx]; const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
let isCluster = false; let isCluster = false;
if (sortedTxSet.length > 1) { if (sortedTxSet.length > 1) {
cpfpClusters[nextTx.txid] = sortedTxSet.map(tx => tx.txid); cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
isCluster = true; isCluster = true;
} }
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4); const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / (nextTx.ancestorWeight / 4));
const used: AuditTransaction[] = []; const used: AuditTransaction[] = [];
while (sortedTxSet.length) { while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop(); const ancestor = sortedTxSet.pop();
const mempoolTx = mempool[ancestor.txid]; const mempoolTx = mempool.get(ancestor.uid);
if (!mempoolTx) {
continue;
}
ancestor.used = true; ancestor.used = true;
ancestor.usedBy = nextTx.txid; ancestor.usedBy = nextTx.uid;
// update original copy of this tx with effective fee rate & relatives data // update original copy of this tx with effective fee rate & relatives data
mempoolTx.effectiveFeePerVsize = effectiveFeeRate; if (mempoolTx.effectiveFeePerVsize !== effectiveFeeRate) {
if (isCluster) { mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
mempoolTx.cpfpRoot = nextTx.txid; mempoolTx.dirty = true;
}
if (mempoolTx.cpfpRoot !== nextTx.uid) {
mempoolTx.cpfpRoot = isCluster ? nextTx.uid : null;
mempoolTx.dirty;
} }
mempoolTx.cpfpChecked = true; mempoolTx.cpfpChecked = true;
transactions.push(ancestor); transactions.push(ancestor);
blockSize += ancestor.size;
blockWeight += ancestor.weight; blockWeight += ancestor.weight;
used.push(ancestor); used.push(ancestor);
} }
@@ -138,7 +158,7 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
// remove these as valid package ancestors for any descendants remaining in the mempool // remove these as valid package ancestors for any descendants remaining in the mempool
if (used.length) { if (used.length) {
used.forEach(tx => { used.forEach(tx => {
updateDescendants(tx, auditPool, modified); updateDescendants(tx, auditPool, modified, effectiveFeeRate);
}); });
} }
@@ -156,11 +176,10 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
if ((exceededPackageTries || queueEmpty) && blocks.length < 7) { if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
// construct this block // construct this block
if (transactions.length) { if (transactions.length) {
blocks.push(transactions.map(t => mempool[t.txid])); blocks.push(transactions.map(t => t.uid));
} }
// reset for the next block // reset for the next block
transactions = []; transactions = [];
blockSize = 0;
blockWeight = 4000; blockWeight = 4000;
// 'overflow' packages didn't fit in this block, but are valid candidates for the next // 'overflow' packages didn't fit in this block, but are valid candidates for the next
@@ -175,50 +194,38 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
overflow = []; overflow = [];
} }
} }
// pack any leftover transactions into the last block
for (const tx of overflow) { if (overflow.length > 0) {
if (!tx || tx?.used) { logger.warn('GBT overflow list unexpectedly non-empty after final block constructed');
continue;
}
blockWeight += tx.weight;
const mempoolTx = mempool[tx.txid];
// update original copy of this tx with effective fee rate & relatives data
mempoolTx.effectiveFeePerVsize = tx.score;
if (tx.ancestorMap.size > 0) {
cpfpClusters[tx.txid] = Array.from(tx.ancestorMap?.values()).map(a => a.txid);
mempoolTx.cpfpRoot = tx.txid;
}
mempoolTx.cpfpChecked = true;
transactions.push(tx);
tx.used = true;
} }
const blockTransactions = transactions.map(t => mempool[t.txid]); // add the final unbounded block if it contains any transactions
restOfArray.forEach(tx => { if (transactions.length > 0) {
blockWeight += tx.weight; blocks.push(transactions.map(t => t.uid));
tx.effectiveFeePerVsize = tx.feePerVsize; }
tx.cpfpChecked = false;
blockTransactions.push(tx); // get map of dirty transactions
}); const rates = new Map<number, number>();
if (blockTransactions.length) { for (const tx of mempool.values()) {
blocks.push(blockTransactions); if (tx?.dirty) {
rates.set(tx.uid, tx.effectiveFeePerVsize || tx.feePerVsize);
}
} }
transactions = [];
const end = Date.now(); const end = Date.now();
const time = end - start; const time = end - start;
logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds'); logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
return { blocks, clusters: cpfpClusters }; return { blocks, rates, clusters: cpfpClusters };
} }
// traverse in-mempool ancestors // traverse in-mempool ancestors
// recursion unavoidable, but should be limited to depth < 25 by mempool policy // recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives( function setRelatives(
tx: AuditTransaction, tx: AuditTransaction,
mempool: { [txid: string]: AuditTransaction }, mempool: Map<number, AuditTransaction>,
): void { ): void {
for (const parent of tx.vin) { for (const parent of tx.inputs) {
const parentTx = mempool[parent]; const parentTx = mempool.get(parent);
if (parentTx && !tx.ancestorMap?.has(parent)) { if (parentTx && !tx.ancestorMap?.has(parent)) {
tx.ancestorMap.set(parent, parentTx); tx.ancestorMap.set(parent, parentTx);
parentTx.children.add(tx); parentTx.children.add(tx);
@@ -227,15 +234,17 @@ function setRelatives(
setRelatives(parentTx, mempool); setRelatives(parentTx, mempool);
} }
parentTx.ancestorMap.forEach((ancestor) => { parentTx.ancestorMap.forEach((ancestor) => {
tx.ancestorMap.set(ancestor.txid, ancestor); tx.ancestorMap.set(ancestor.uid, ancestor);
}); });
} }
}; };
tx.ancestorFee = tx.fee || 0; tx.ancestorFee = tx.fee || 0;
tx.ancestorWeight = tx.weight || 0; tx.ancestorWeight = tx.weight || 0;
tx.ancestorSigops = tx.sigops || 0;
tx.ancestorMap.forEach((ancestor) => { tx.ancestorMap.forEach((ancestor) => {
tx.ancestorFee += ancestor.fee; tx.ancestorFee += ancestor.fee;
tx.ancestorWeight += ancestor.weight; tx.ancestorWeight += ancestor.weight;
tx.ancestorSigops += ancestor.sigops;
}); });
tx.score = tx.ancestorFee / ((tx.ancestorWeight / 4) || 1); tx.score = tx.ancestorFee / ((tx.ancestorWeight / 4) || 1);
tx.relativesSet = true; tx.relativesSet = true;
@@ -245,8 +254,9 @@ function setRelatives(
// avoids recursion to limit call stack depth // avoids recursion to limit call stack depth
function updateDescendants( function updateDescendants(
rootTx: AuditTransaction, rootTx: AuditTransaction,
mempool: { [txid: string]: AuditTransaction }, mempool: Map<number, AuditTransaction>,
modified: PairingHeap<AuditTransaction>, modified: PairingHeap<AuditTransaction>,
clusterRate: number,
): void { ): void {
const descendantSet: Set<AuditTransaction> = new Set(); const descendantSet: Set<AuditTransaction> = new Set();
// stack of nodes left to visit // stack of nodes left to visit
@@ -261,13 +271,15 @@ function updateDescendants(
}); });
while (descendants.length) { while (descendants.length) {
descendantTx = descendants.pop(); descendantTx = descendants.pop();
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) { if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.uid)) {
// remove tx as ancestor // remove tx as ancestor
descendantTx.ancestorMap.delete(rootTx.txid); descendantTx.ancestorMap.delete(rootTx.uid);
descendantTx.ancestorFee -= rootTx.fee; descendantTx.ancestorFee -= rootTx.fee;
descendantTx.ancestorWeight -= rootTx.weight; descendantTx.ancestorWeight -= rootTx.weight;
descendantTx.ancestorSigops -= rootTx.sigops;
tmpScore = descendantTx.score; tmpScore = descendantTx.score;
descendantTx.score = descendantTx.ancestorFee / (descendantTx.ancestorWeight / 4); descendantTx.score = descendantTx.ancestorFee / (descendantTx.ancestorWeight / 4);
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
if (!descendantTx.modifiedNode) { if (!descendantTx.modifiedNode) {
descendantTx.modified = true; descendantTx.modified = true;

View File

@@ -1,8 +1,8 @@
import logger from '../logger'; import logger from '../logger';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { import {
BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta, BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, IConversionRates OptimizedStatistic, ILoadingIndicators
} from '../mempool.interfaces'; } from '../mempool.interfaces';
import blocks from './blocks'; import blocks from './blocks';
import memPool from './mempool'; import memPool from './mempool';
@@ -20,11 +20,20 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
import Audit from './audit'; import Audit from './audit';
import { deepClone } from '../utils/clone'; import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
import accelerationApi from './services/acceleration';
class WebsocketHandler { class WebsocketHandler {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
private extraInitProperties = {}; private extraInitProperties = {};
private numClients = 0;
private numConnected = 0;
private numDisconnected = 0;
private initData: { [key: string]: string } = {};
private serializedInitData: string = '{}';
constructor() { } constructor() { }
setWebsocketServer(wss: WebSocket.Server) { setWebsocketServer(wss: WebSocket.Server) {
@@ -33,6 +42,41 @@ class WebsocketHandler {
setExtraInitProperties(property: string, value: any) { setExtraInitProperties(property: string, value: any) {
this.extraInitProperties[property] = value; this.extraInitProperties[property] = value;
this.setInitDataFields(this.extraInitProperties);
}
private setInitDataFields(data: { [property: string]: any }): void {
for (const property of Object.keys(data)) {
if (data[property] != null) {
this.initData[property] = JSON.stringify(data[property]);
} else {
delete this.initData[property];
}
}
this.serializedInitData = '{'
+ Object.keys(this.initData).map(key => `"${key}": ${this.initData[key]}`).join(', ')
+ '}';
}
private updateInitData(): void {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
const da = difficultyAdjustment.getDifficultyAdjustment();
this.setInitDataFields({
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks,
'conversions': priceUpdater.getLatestPrices(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined,
'fees': feeApi.getRecommendedFee(),
});
}
public getSerializedInitData(): string {
return this.serializedInitData;
} }
setupConnectionHandling() { setupConnectionHandling() {
@@ -41,7 +85,11 @@ class WebsocketHandler {
} }
this.wss.on('connection', (client: WebSocket) => { this.wss.on('connection', (client: WebSocket) => {
this.numConnected++;
client.on('error', logger.info); client.on('error', logger.info);
client.on('close', () => {
this.numDisconnected++;
});
client.on('message', async (message: string) => { client.on('message', async (message: string) => {
try { try {
const parsedMessage: WebsocketResponse = JSON.parse(message); const parsedMessage: WebsocketResponse = JSON.parse(message);
@@ -57,9 +105,10 @@ class WebsocketHandler {
if (parsedMessage && parsedMessage['track-tx']) { if (parsedMessage && parsedMessage['track-tx']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
client['track-tx'] = parsedMessage['track-tx']; client['track-tx'] = parsedMessage['track-tx'];
const trackTxid = client['track-tx'];
// Client is telling the transaction wasn't found // Client is telling the transaction wasn't found
if (parsedMessage['watch-mempool']) { if (parsedMessage['watch-mempool']) {
const rbfCacheTxid = rbfCache.getReplacedBy(client['track-tx']); const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid);
if (rbfCacheTxid) { if (rbfCacheTxid) {
response['txReplaced'] = { response['txReplaced'] = {
txid: rbfCacheTxid, txid: rbfCacheTxid,
@@ -67,14 +116,14 @@ class WebsocketHandler {
client['track-tx'] = null; client['track-tx'] = null;
} else { } else {
// It might have appeared before we had the time to start watching for it // It might have appeared before we had the time to start watching for it
const tx = memPool.getMempool()[client['track-tx']]; const tx = memPool.getMempool()[trackTxid];
if (tx) { if (tx) {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
response['tx'] = tx; response['tx'] = tx;
} else { } else {
// tx.prevout is missing from transactions when in bitcoind mode // tx.prevout is missing from transactions when in bitcoind mode
try { try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true); const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
response['tx'] = fullTx; response['tx'] = fullTx;
} catch (e) { } catch (e) {
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e)); logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
@@ -82,7 +131,7 @@ class WebsocketHandler {
} }
} else { } else {
try { try {
const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true); const fullTx = await transactionUtils.$getMempoolTransactionExtended(client['track-tx'], true);
response['tx'] = fullTx; response['tx'] = fullTx;
} catch (e) { } catch (e) {
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e)); logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
@@ -91,6 +140,14 @@ class WebsocketHandler {
} }
} }
} }
const tx = memPool.getMempool()[trackTxid];
if (tx && tx.position) {
response['txPosition'] = {
txid: trackTxid,
position: tx.position,
accelerated: tx.acceleration || undefined,
};
}
} else { } else {
client['track-tx'] = null; client['track-tx'] = null;
} }
@@ -131,12 +188,22 @@ class WebsocketHandler {
} }
} }
if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
client['track-rbf'] = parsedMessage['track-rbf'];
} else {
client['track-rbf'] = false;
}
}
if (parsedMessage.action === 'init') { if (parsedMessage.action === 'init') {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT); if (!this.initData['blocks']?.length || !this.initData['da']) {
if (!_blocks) { this.updateInitData();
}
if (!this.initData['blocks']?.length) {
return; return;
} }
client.send(JSON.stringify(this.getInitData(_blocks))); client.send(this.serializedInitData);
} }
if (parsedMessage.action === 'ping') { if (parsedMessage.action === 'ping') {
@@ -185,51 +252,44 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.setInitDataFields({ 'loadingIndicators': indicators });
const response = JSON.stringify({ loadingIndicators: indicators });
this.wss.clients.forEach((client) => { this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) { if (client.readyState !== WebSocket.OPEN) {
return; return;
} }
client.send(JSON.stringify({ loadingIndicators: indicators })); client.send(response);
}); });
} }
handleNewConversionRates(conversionRates: IConversionRates) { handleNewConversionRates(conversionRates: ApiPrice) {
if (!this.wss) { if (!this.wss) {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.setInitDataFields({ 'conversions': conversionRates });
const response = JSON.stringify({ conversions: conversionRates });
this.wss.clients.forEach((client) => { this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) { if (client.readyState !== WebSocket.OPEN) {
return; return;
} }
client.send(JSON.stringify({ conversions: conversionRates })); client.send(response);
}); });
} }
getInitData(_blocks?: BlockExtended[]) {
if (!_blocks) {
_blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
}
return {
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks,
'conversions': priceUpdater.latestPrices,
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': difficultyAdjustment.getDifficultyAdjustment(),
'fees': feeApi.getRecommendedFee(),
...this.extraInitProperties
};
}
handleNewStatistic(stats: OptimizedStatistic) { handleNewStatistic(stats: OptimizedStatistic) {
if (!this.wss) { if (!this.wss) {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.printLogs();
const response = JSON.stringify({
'live-2h-chart': stats
});
this.wss.clients.forEach((client) => { this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) { if (client.readyState !== WebSocket.OPEN) {
return; return;
@@ -239,20 +299,20 @@ class WebsocketHandler {
return; return;
} }
client.send(JSON.stringify({ client.send(response);
'live-2h-chart': stats
}));
}); });
} }
async handleMempoolChange(newMempool: { [txid: string]: TransactionExtended }, async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended },
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]): Promise<void> {
if (!this.wss) { if (!this.wss) {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.printLogs();
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid), true); await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
} else { } else {
mempoolBlocks.updateMempoolBlocks(newMempool, true); mempoolBlocks.updateMempoolBlocks(newMempool, true);
} }
@@ -264,8 +324,57 @@ class WebsocketHandler {
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
memPool.handleRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements;
let fullRbfReplacements;
if (Object.keys(rbfChanges.trees).length) {
rbfReplacements = rbfCache.getRbfTrees(false);
fullRbfReplacements = rbfCache.getRbfTrees(true);
}
for (const deletedTx of deletedTransactions) {
rbfCache.evict(deletedTx.txid);
}
memPool.removeFromSpendMap(deletedTransactions);
memPool.addToSpendMap(newTransactions);
const recommendedFees = feeApi.getRecommendedFee(); const recommendedFees = feeApi.getRecommendedFee();
// update init data
this.updateInitData();
// cache serialized objects to avoid stringify-ing the same thing for every client
const responseCache = { ...this.initData };
function getCachedResponse(key: string, data): string {
if (!responseCache[key]) {
responseCache[key] = JSON.stringify(data);
}
return responseCache[key];
}
// pre-compute new tracked outspends
const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {};
const trackedTxs = new Set<string>();
this.wss.clients.forEach((client) => {
if (client['track-tx']) {
trackedTxs.add(client['track-tx']);
}
});
if (trackedTxs.size > 0) {
for (const tx of newTransactions) {
for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[i];
if (trackedTxs.has(vin.txid)) {
if (!outspendCache[vin.txid]) {
outspendCache[vin.txid] = { [vin.vout]: { vin: i, txid: tx.txid }};
} else {
outspendCache[vin.txid][vin.vout] = { vin: i, txid: tx.txid };
}
}
}
}
}
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
this.wss.clients.forEach(async (client) => { this.wss.clients.forEach(async (client) => {
if (client.readyState !== WebSocket.OPEN) { if (client.readyState !== WebSocket.OPEN) {
return; return;
@@ -274,15 +383,17 @@ class WebsocketHandler {
const response = {}; const response = {};
if (client['want-stats']) { if (client['want-stats']) {
response['mempoolInfo'] = mempoolInfo; response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
response['vBytesPerSecond'] = vBytesPerSecond; response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', vBytesPerSecond);
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx)); response['transactions'] = getCachedResponse('transactions', latestTransactions);
response['da'] = da; if (da?.previousTime) {
response['fees'] = recommendedFees; response['da'] = getCachedResponse('da', da);
}
response['fees'] = getCachedResponse('fees', recommendedFees);
} }
if (client['want-mempool-blocks']) { if (client['want-mempool-blocks']) {
response['mempool-blocks'] = mBlocks; response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
} }
if (client['track-mempool-tx']) { if (client['track-mempool-tx']) {
@@ -290,13 +401,13 @@ class WebsocketHandler {
if (tx) { if (tx) {
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
try { try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true); const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
response['tx'] = fullTx; response['tx'] = JSON.stringify(fullTx);
} catch (e) { } catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
} }
} else { } else {
response['tx'] = tx; response['tx'] = JSON.stringify(tx);
} }
client['track-mempool-tx'] = null; client['track-mempool-tx'] = null;
} }
@@ -310,7 +421,7 @@ class WebsocketHandler {
if (someVin) { if (someVin) {
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
try { try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true); const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx); foundTransactions.push(fullTx);
} catch (e) { } catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
@@ -324,7 +435,7 @@ class WebsocketHandler {
if (someVout) { if (someVout) {
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
try { try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true); const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx); foundTransactions.push(fullTx);
} catch (e) { } catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
@@ -336,7 +447,7 @@ class WebsocketHandler {
} }
if (foundTransactions.length) { if (foundTransactions.length) {
response['address-transactions'] = foundTransactions; response['address-transactions'] = JSON.stringify(foundTransactions);
} }
} }
@@ -365,49 +476,60 @@ class WebsocketHandler {
}); });
if (foundTransactions.length) { if (foundTransactions.length) {
response['address-transactions'] = foundTransactions; response['address-transactions'] = JSON.stringify(foundTransactions);
} }
} }
if (client['track-tx']) { if (client['track-tx']) {
const outspends: object = {}; const trackTxid = client['track-tx'];
newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => { const outspends = outspendCache[trackTxid];
if (vin.txid === client['track-tx']) {
outspends[vin.vout] = {
vin: i,
txid: tx.txid,
};
}
}));
if (Object.keys(outspends).length) { if (outspends && Object.keys(outspends).length) {
response['utxoSpent'] = outspends; response['utxoSpent'] = JSON.stringify(outspends);
} }
if (rbfTransactions[client['track-tx']]) { const rbfReplacedBy = rbfCache.getReplacedBy(client['track-tx']);
for (const rbfTransaction in rbfTransactions) { if (rbfReplacedBy) {
if (client['track-tx'] === rbfTransaction) { response['rbfTransaction'] = JSON.stringify({
response['rbfTransaction'] = { txid: rbfReplacedBy,
txid: rbfTransactions[rbfTransaction].txid, })
}; }
break;
} const rbfChange = rbfChanges.map[client['track-tx']];
} if (rbfChange) {
response['rbfInfo'] = JSON.stringify(rbfChanges.trees[rbfChange]);
}
const mempoolTx = newMempool[trackTxid];
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position: mempoolTx.position,
});
} }
} }
if (client['track-mempool-block'] >= 0) { if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block']; const index = client['track-mempool-block'];
if (mBlockDeltas[index]) { if (mBlockDeltas[index]) {
response['projected-block-transactions'] = { response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
index: index, index: index,
delta: mBlockDeltas[index], delta: mBlockDeltas[index],
}; });
} }
} }
if (client['track-rbf'] === 'all' && rbfReplacements) {
response['rbfLatest'] = getCachedResponse('rbfLatest', rbfReplacements);
} else if (client['track-rbf'] === 'fullRbf' && fullRbfReplacements) {
response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements);
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(JSON.stringify(response)); const serializedResponse = '{'
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
+ '}';
client.send(serializedResponse);
} }
}); });
} }
@@ -417,21 +539,34 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.printLogs();
const _memPool = memPool.getMempool(); const _memPool = memPool.getMempool();
if (config.MEMPOOL.AUDIT) { if (config.MEMPOOL.AUDIT) {
let projectedBlocks; let projectedBlocks;
let auditMempool = _memPool;
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && await accelerationApi.$isAcceleratedBlock(block);
// template calculation functions have mempool side effects, so calculate audits using // template calculation functions have mempool side effects, so calculate audits using
// a cloned copy of the mempool if we're running a different algorithm for mempool updates // a cloned copy of the mempool if we're running a different algorithm for mempool updates
const auditMempool = (config.MEMPOOL.ADVANCED_GBT_AUDIT === config.MEMPOOL.ADVANCED_GBT_MEMPOOL) ? _memPool : deepClone(_memPool); const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) { if (separateAudit) {
projectedBlocks = await mempoolBlocks.makeBlockTemplates(auditMempool, false); auditMempool = deepClone(_memPool);
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated);
} else {
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
}
} else { } else {
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false); if ((config.MEMPOOL_SERVICES.ACCELERATIONS && !isAccelerated)) {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated);
} else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
}
} }
if (Common.indexingEnabled() && memPool.isInSync()) { if (Common.indexingEnabled() && memPool.isInSync()) {
const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const { censored, added, fresh, sigop, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100; const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
@@ -458,25 +593,35 @@ class WebsocketHandler {
addedTxs: added, addedTxs: added,
missingTxs: censored, missingTxs: censored,
freshTxs: fresh, freshTxs: fresh,
sigopTxs: sigop,
acceleratedTxs: accelerated,
matchRate: matchRate, matchRate: matchRate,
}); });
if (block.extras) { if (block.extras) {
block.extras.matchRate = matchRate; block.extras.matchRate = matchRate;
block.extras.similarity = similarity;
} }
} }
} else if (block.extras) {
const mBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
if (mBlocks?.length && mBlocks[0].transactions) {
block.extras.similarity = Common.getSimilarity(mBlocks[0], transactions);
}
} }
const removed: string[] = []; const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
// Update mempool to remove transactions included in the new block // Update mempool to remove transactions included in the new block
for (const txId of txIds) { for (const txId of txIds) {
delete _memPool[txId]; delete _memPool[txId];
removed.push(txId); rbfCache.mined(txId);
rbfCache.evict(txId);
} }
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.updateBlockTemplates(_memPool, [], removed, true); await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
} else { } else {
mempoolBlocks.updateMempoolBlocks(_memPool, true); mempoolBlocks.updateMempoolBlocks(_memPool, true);
} }
@@ -486,6 +631,19 @@ class WebsocketHandler {
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
const fees = feeApi.getRecommendedFee(); const fees = feeApi.getRecommendedFee();
// update init data
this.updateInitData();
const responseCache = { ...this.initData };
function getCachedResponse(key, data): string {
if (!responseCache[key]) {
responseCache[key] = JSON.stringify(data);
}
return responseCache[key];
}
const mempoolInfo = memPool.getMempoolInfo();
this.wss.clients.forEach((client) => { this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) { if (client.readyState !== WebSocket.OPEN) {
return; return;
@@ -495,19 +653,29 @@ class WebsocketHandler {
return; return;
} }
const response = { const response = {};
'block': block, response['block'] = getCachedResponse('block', block);
'mempoolInfo': memPool.getMempoolInfo(), response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
'da': da, response['da'] = getCachedResponse('da', da?.previousTime ? da : undefined);
'fees': fees, response['fees'] = getCachedResponse('fees', fees);
};
if (mBlocks && client['want-mempool-blocks']) { if (mBlocks && client['want-mempool-blocks']) {
response['mempool-blocks'] = mBlocks; response['mempool-blocks'] = getCachedResponse('mempool-blocks', mBlocks);
} }
if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) { if (client['track-tx']) {
response['txConfirmed'] = true; const trackTxid = client['track-tx'];
if (trackTxid && txIds.indexOf(trackTxid) > -1) {
response['txConfirmed'] = JSON.stringify(trackTxid);
} else {
const mempoolTx = _memPool[trackTxid];
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position: mempoolTx.position,
});
}
}
} }
if (client['track-address']) { if (client['track-address']) {
@@ -533,7 +701,7 @@ class WebsocketHandler {
}; };
}); });
response['block-transactions'] = foundTransactions; response['block-transactions'] = JSON.stringify(foundTransactions);
} }
} }
@@ -570,23 +738,37 @@ class WebsocketHandler {
}; };
}); });
response['block-transactions'] = foundTransactions; response['block-transactions'] = JSON.stringify(foundTransactions);
} }
} }
if (client['track-mempool-block'] >= 0) { if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block']; const index = client['track-mempool-block'];
if (mBlockDeltas && mBlockDeltas[index]) { if (mBlockDeltas && mBlockDeltas[index]) {
response['projected-block-transactions'] = { response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
index: index, index: index,
delta: mBlockDeltas[index], delta: mBlockDeltas[index],
}; });
} }
} }
client.send(JSON.stringify(response)); const serializedResponse = '{'
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
+ '}';
client.send(serializedResponse);
}); });
} }
private printLogs(): void {
if (this.wss) {
const count = this.wss?.clients?.size || 0;
const diff = count - this.numClients;
this.numClients = count;
logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`);
this.numConnected = 0;
this.numDisconnected = 0;
}
}
} }
export default new WebsocketHandler(); export default new WebsocketHandler();

View File

@@ -33,9 +33,12 @@ interface IConfig {
ADVANCED_GBT_MEMPOOL: boolean; ADVANCED_GBT_MEMPOOL: boolean;
CPFP_INDEXING: boolean; CPFP_INDEXING: boolean;
MAX_BLOCKS_BULK_QUERY: number; MAX_BLOCKS_BULK_QUERY: number;
DISK_CACHE_BLOCK_INTERVAL: number;
}; };
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null;
RETRY_UNIX_SOCKET_AFTER: number;
}; };
LIGHTNING: { LIGHTNING: {
ENABLED: boolean; ENABLED: boolean;
@@ -51,6 +54,7 @@ interface IConfig {
TLS_CERT_PATH: string; TLS_CERT_PATH: string;
MACAROON_PATH: string; MACAROON_PATH: string;
REST_API_URL: string; REST_API_URL: string;
TIMEOUT: number;
}; };
CLIGHTNING: { CLIGHTNING: {
SOCKET: string; SOCKET: string;
@@ -65,12 +69,14 @@ interface IConfig {
PORT: number; PORT: number;
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number;
}; };
SECOND_CORE_RPC: { SECOND_CORE_RPC: {
HOST: string; HOST: string;
PORT: number; PORT: number;
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number;
}; };
DATABASE: { DATABASE: {
ENABLED: boolean; ENABLED: boolean;
@@ -80,6 +86,7 @@ interface IConfig {
DATABASE: string; DATABASE: string;
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number;
}; };
SYSLOG: { SYSLOG: {
ENABLED: boolean; ENABLED: boolean;
@@ -122,6 +129,10 @@ interface IConfig {
GEOLITE2_ASN: string; GEOLITE2_ASN: string;
GEOIP2_ISP: string; GEOIP2_ISP: string;
}, },
MEMPOOL_SERVICES: {
API: string;
ACCELERATIONS: boolean;
},
} }
const defaults: IConfig = { const defaults: IConfig = {
@@ -155,9 +166,12 @@ const defaults: IConfig = {
'ADVANCED_GBT_MEMPOOL': false, 'ADVANCED_GBT_MEMPOOL': false,
'CPFP_INDEXING': false, 'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0, 'MAX_BLOCKS_BULK_QUERY': 0,
'DISK_CACHE_BLOCK_INTERVAL': 6,
}, },
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null,
'RETRY_UNIX_SOCKET_AFTER': 30000,
}, },
'ELECTRUM': { 'ELECTRUM': {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
@@ -168,13 +182,15 @@ const defaults: IConfig = {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
'PORT': 8332, 'PORT': 8332,
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool' 'PASSWORD': 'mempool',
'TIMEOUT': 60000,
}, },
'SECOND_CORE_RPC': { 'SECOND_CORE_RPC': {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
'PORT': 8332, 'PORT': 8332,
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool' 'PASSWORD': 'mempool',
'TIMEOUT': 60000,
}, },
'DATABASE': { 'DATABASE': {
'ENABLED': true, 'ENABLED': true,
@@ -183,7 +199,8 @@ const defaults: IConfig = {
'PORT': 3306, 'PORT': 3306,
'DATABASE': 'mempool', 'DATABASE': 'mempool',
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool' 'PASSWORD': 'mempool',
'TIMEOUT': 180000,
}, },
'SYSLOG': { 'SYSLOG': {
'ENABLED': true, 'ENABLED': true,
@@ -214,6 +231,7 @@ const defaults: IConfig = {
'TLS_CERT_PATH': '', 'TLS_CERT_PATH': '',
'MACAROON_PATH': '', 'MACAROON_PATH': '',
'REST_API_URL': 'https://localhost:8080', 'REST_API_URL': 'https://localhost:8080',
'TIMEOUT': 10000,
}, },
'CLIGHTNING': { 'CLIGHTNING': {
'SOCKET': '', 'SOCKET': '',
@@ -244,6 +262,10 @@ const defaults: IConfig = {
'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb', 'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb' 'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
}, },
'MEMPOOL_SERVICES': {
'API': '',
'ACCELERATIONS': false,
}
}; };
class Config implements IConfig { class Config implements IConfig {
@@ -263,6 +285,7 @@ class Config implements IConfig {
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER']; PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER']; EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
MAXMIND: IConfig['MAXMIND']; MAXMIND: IConfig['MAXMIND'];
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
constructor() { constructor() {
const configs = this.merge(configFromFile, defaults); const configs = this.merge(configFromFile, defaults);
@@ -282,6 +305,7 @@ class Config implements IConfig {
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER; this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER; this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
this.MAXMIND = configs.MAXMIND; this.MAXMIND = configs.MAXMIND;
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
} }
merge = (...objects: object[]): IConfig => { merge = (...objects: object[]): IConfig => {

View File

@@ -33,8 +33,32 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]> OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]>
{ {
this.checkDBFlag(); this.checkDBFlag();
const pool = await this.getPool(); let hardTimeout;
return pool.query(query, params); if (query?.timeout != null) {
hardTimeout = Math.floor(query.timeout * 1.1);
} else {
hardTimeout = config.DATABASE.TIMEOUT;
}
if (hardTimeout > 0) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`DB query failed to return, reject or time out within ${hardTimeout / 1000}s - ${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}`));
}, hardTimeout);
this.getPool().then(pool => {
return pool.query(query, params) as Promise<[T, FieldPacket[]]>;
}).then(result => {
resolve(result);
}).catch(error => {
reject(error);
}).finally(() => {
clearTimeout(timer);
});
});
} else {
const pool = await this.getPool();
return pool.query(query, params);
}
} }
public async checkDbConnection() { public async checkDbConnection() {

View File

@@ -2,6 +2,7 @@ import express from 'express';
import { Application, Request, Response, NextFunction } from 'express'; import { Application, Request, Response, NextFunction } from 'express';
import * as http from 'http'; import * as http from 'http';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
import cluster from 'cluster'; import cluster from 'cluster';
import DB from './database'; import DB from './database';
import config from './config'; import config from './config';
@@ -38,12 +39,20 @@ import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater'; import priceUpdater from './tasks/price-updater';
import chainTips from './api/chain-tips'; import chainTips from './api/chain-tips';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
private server: http.Server | undefined; private server: http.Server | undefined;
private app: Application; private app: Application;
private currentBackendRetryInterval = 5; private currentBackendRetryInterval = 1;
private backendRetryCount = 0;
private maxHeapSize: number = 0;
private heapLogInterval: number = 60;
private warnedHeapCritical: boolean = false;
private lastHeapLogTime: number | null = null;
constructor() { constructor() {
this.app = express(); this.app = express();
@@ -113,7 +122,7 @@ class Server {
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$(); await syncAssets.syncAssets$();
if (config.MEMPOOL.ENABLED) { if (config.MEMPOOL.ENABLED) {
diskCache.loadMempoolCache(); await diskCache.$loadMempoolCache();
} }
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) { if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
@@ -137,6 +146,8 @@ class Server {
this.runMainUpdateLoop(); this.runMainUpdateLoop();
} }
setInterval(() => { this.healthCheck(); }, 2500);
if (config.BISQ.ENABLED) { if (config.BISQ.ENABLED) {
bisq.startBisqService(); bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price)); bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
@@ -169,22 +180,26 @@ class Server {
logger.debug(msg); logger.debug(msg);
} }
} }
await blocks.$updateBlocks(); const newMempool = await bitcoinApi.$getRawMempool();
await memPool.$updateMempool(); const numHandledBlocks = await blocks.$updateBlocks();
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool);
}
indexer.$run(); indexer.$run();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS); // rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
this.currentBackendRetryInterval = 5; setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 1 : config.MEMPOOL.POLL_RATE_MS);
this.backendRetryCount = 0;
} catch (e: any) { } catch (e: any) {
let loggerMsg = `Exception in runMainUpdateLoop(). Retrying in ${this.currentBackendRetryInterval} sec.`; this.backendRetryCount++;
let loggerMsg = `Exception in runMainUpdateLoop() (count: ${this.backendRetryCount}). Retrying in ${this.currentBackendRetryInterval} sec.`;
loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`; loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`;
if (e?.stack) { if (e?.stack) {
loggerMsg += ` Stack trace: ${e.stack}`; loggerMsg += ` Stack trace: ${e.stack}`;
} }
// When we get a first Exception, only `logger.debug` it and retry after 5 seconds // When we get a first Exception, only `logger.debug` it and retry after 5 seconds
// From the second Exception, `logger.warn` the Exception and increase the retry delay // From the second Exception, `logger.warn` the Exception and increase the retry delay
// Maximum retry delay is 60 seconds if (this.backendRetryCount >= 5) {
if (this.currentBackendRetryInterval > 5) {
logger.warn(loggerMsg); logger.warn(loggerMsg);
mempool.setOutOfSync(); mempool.setOutOfSync();
} else { } else {
@@ -194,8 +209,8 @@ class Server {
logger.debug(`AxiosError: ${e?.message}`); logger.debug(`AxiosError: ${e?.message}`);
} }
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval); setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
this.currentBackendRetryInterval *= 2; } finally {
this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60); diskCache.unlock();
} }
} }
@@ -206,11 +221,11 @@ class Server {
await lightningStatsUpdater.$startService(); await lightningStatsUpdater.$startService();
await forensicsService.$startService(); await forensicsService.$startService();
} catch(e) { } catch(e) {
logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`); logger.err(`Exception in $runLightningBackend. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
await Common.sleep$(1000 * 60); await Common.sleep$(1000 * 60);
this.$runLightningBackend(); this.$runLightningBackend();
}; };
} }
setUpWebsocketHandling(): void { setUpWebsocketHandling(): void {
if (this.wss) { if (this.wss) {
@@ -228,7 +243,7 @@ class Server {
websocketHandler.setupConnectionHandling(); websocketHandler.setupConnectionHandling();
if (config.MEMPOOL.ENABLED) { if (config.MEMPOOL.ENABLED) {
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler)); statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler)); memPool.setAsyncMempoolChangedCallback(websocketHandler.$handleMempoolChange.bind(websocketHandler));
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler)); blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
} }
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler)); priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
@@ -255,6 +270,26 @@ class Server {
channelsRoutes.initRoutes(this.app); channelsRoutes.initRoutes(this.app);
} }
} }
healthCheck(): void {
const now = Date.now();
const stats = v8.getHeapStatistics();
this.maxHeapSize = Math.max(stats.used_heap_size, this.maxHeapSize);
const warnThreshold = 0.8 * stats.heap_size_limit;
const byteUnits = getBytesUnit(Math.max(this.maxHeapSize, stats.heap_size_limit));
if (!this.warnedHeapCritical && this.maxHeapSize > warnThreshold) {
this.warnedHeapCritical = true;
logger.warn(`Used ${(this.maxHeapSize / stats.heap_size_limit * 100).toFixed(2)}% of heap limit (${formatBytes(this.maxHeapSize, byteUnits, true)} / ${formatBytes(stats.heap_size_limit, byteUnits)})!`);
}
if (this.lastHeapLogTime === null || (now - this.lastHeapLogTime) > (this.heapLogInterval * 1000)) {
logger.debug(`Memory usage: ${formatBytes(this.maxHeapSize, byteUnits)} / ${formatBytes(stats.heap_size_limit, byteUnits)}`);
this.warnedHeapCritical = false;
this.maxHeapSize = 0;
this.lastHeapLogTime = now;
}
}
} }
((): Server => new Server())(); ((): Server => new Server())();

View File

@@ -76,13 +76,13 @@ class Indexer {
this.tasksRunning.push(task); this.tasksRunning.push(task);
const lastestPriceId = await PricesRepository.$getLatestPriceId(); const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === false || lastestPriceId === null) { if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`); logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
setTimeout(() => { setTimeout(() => {
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
this.runSingleTask('blocksPrices'); this.runSingleTask('blocksPrices');
}, 10000); }, 10000);
} else { } else {
logger.debug(`Blocks prices indexer will run now`); logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices(); await mining.$indexBlockPrices();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
} }
@@ -112,7 +112,7 @@ class Indexer {
this.runIndexer = false; this.runIndexer = false;
this.indexerRunning = true; this.indexerRunning = true;
logger.info(`Running mining indexer`); logger.debug(`Running mining indexer`);
await this.checkAvailableCoreIndexes(); await this.checkAvailableCoreIndexes();
@@ -122,7 +122,7 @@ class Indexer {
const chainValid = await blocks.$generateBlockDatabase(); const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) { if (chainValid === false) {
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration // Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`); logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`, logger.tags.mining);
setTimeout(() => this.reindex(), 10000); setTimeout(() => this.reindex(), 10000);
this.indexerRunning = false; this.indexerRunning = false;
return; return;

View File

@@ -69,6 +69,10 @@ class Logger {
this.network = this.getNetwork(); this.network = this.getNetwork();
} }
public updateNetwork(): void {
this.network = this.getNetwork();
}
private addprio(prio): void { private addprio(prio): void {
this[prio] = (function(_this) { this[prio] = (function(_this) {
return function(msg, tag?: string) { return function(msg, tag?: string) {

View File

@@ -32,7 +32,9 @@ export interface BlockAudit {
hash: string, hash: string,
missingTxs: string[], missingTxs: string[],
freshTxs: string[], freshTxs: string[],
sigopTxs: string[],
addedTxs: string[], addedTxs: string[],
acceleratedTxs: string[],
matchRate: number, matchRate: number,
} }
@@ -58,6 +60,7 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
export interface MempoolBlockDelta { export interface MempoolBlockDelta {
added: TransactionStripped[]; added: TransactionStripped[];
removed: string[]; removed: string[];
changed: { txid: string, rate: number | undefined }[];
} }
interface VinStrippedToScriptsig { interface VinStrippedToScriptsig {
@@ -79,25 +82,52 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
descendants?: Ancestor[]; descendants?: Ancestor[];
bestDescendant?: BestDescendant | null; bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean; cpfpChecked?: boolean;
deleteAfter?: number; position?: {
block: number,
vsize: number,
};
acceleration?: number;
uid?: number;
}
export interface MempoolTransactionExtended extends TransactionExtended {
sigops: number;
adjustedVsize: number;
adjustedFeePerVsize: number;
} }
export interface AuditTransaction { export interface AuditTransaction {
txid: string; uid: number;
fee: number; fee: number;
weight: number; weight: number;
feePerVsize: number; feePerVsize: number;
effectiveFeePerVsize: number; effectiveFeePerVsize: number;
vin: string[]; sigops: number;
inputs: number[];
relativesSet: boolean; relativesSet: boolean;
ancestorMap: Map<string, AuditTransaction>; ancestorMap: Map<number, AuditTransaction>;
children: Set<AuditTransaction>; children: Set<AuditTransaction>;
ancestorFee: number; ancestorFee: number;
ancestorWeight: number; ancestorWeight: number;
ancestorSigops: number;
score: number; score: number;
used: boolean; used: boolean;
modified: boolean; modified: boolean;
modifiedNode: HeapNode<AuditTransaction>; modifiedNode: HeapNode<AuditTransaction>;
dependencyRate?: number;
}
export interface CompactThreadTransaction {
uid: number;
fee: number;
weight: number;
sigops: number;
feePerVsize: number;
effectiveFeePerVsize?: number;
inputs: number[];
cpfpRoot?: string;
cpfpChecked?: boolean;
dirty?: boolean;
} }
export interface ThreadTransaction { export interface ThreadTransaction {
@@ -106,7 +136,7 @@ export interface ThreadTransaction {
weight: number; weight: number;
feePerVsize: number; feePerVsize: number;
effectiveFeePerVsize?: number; effectiveFeePerVsize?: number;
vin: string[]; inputs: number[];
cpfpRoot?: string; cpfpRoot?: string;
cpfpChecked?: boolean; cpfpChecked?: boolean;
} }
@@ -145,6 +175,8 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
acc?: number;
rate?: number; // effective fee rate
} }
export interface BlockExtension { export interface BlockExtension {
@@ -153,6 +185,7 @@ export interface BlockExtension {
feeRange: number[]; // fee rate percentiles feeRange: number[]; // fee rate percentiles
reward: number; reward: number;
matchRate: number | null; matchRate: number | null;
similarity?: number;
pool: { pool: {
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id` id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
name: string; name: string;
@@ -213,6 +246,21 @@ export interface MempoolStats {
tx_count: number; tx_count: number;
} }
export interface EffectiveFeeStats {
medianFee: number; // median effective fee rate
feeRange: number[]; // 2nd, 10th, 25th, 50th, 75th, 90th, 98th percentiles
}
export interface WorkingEffectiveFeeStats extends EffectiveFeeStats {
minFee: number;
maxFee: number;
}
export interface CpfpSummary {
transactions: TransactionExtended[];
clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[];
}
export interface Statistic { export interface Statistic {
id?: number; id?: number;
added: string; added: string;
@@ -293,7 +341,6 @@ interface RequiredParams {
} }
export interface ILoadingIndicators { [name: string]: number; } export interface ILoadingIndicators { [name: string]: number; }
export interface IConversionRates { [currency: string]: number; }
export interface IBackendInfo { export interface IBackendInfo {
hostname: string; hostname: string;
@@ -309,9 +356,11 @@ export interface IDifficultyAdjustment {
remainingBlocks: number; remainingBlocks: number;
remainingTime: number; remainingTime: number;
previousRetarget: number; previousRetarget: number;
previousTime: number;
nextRetargetHeight: number; nextRetargetHeight: number;
timeAvg: number; timeAvg: number;
timeOffset: number; timeOffset: number;
expectedBlocks: number;
} }
export interface IndexedDifficultyAdjustment { export interface IndexedDifficultyAdjustment {

View File

@@ -6,20 +6,19 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
class BlocksAuditRepositories { class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> { public async $saveAudit(audit: BlockAudit): Promise<void> {
try { try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, match_rate) await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, accelerated_txs, match_rate)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), audit.matchRate]); JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate]);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`); logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
} else { } else {
logger.err(`Cannot save block audit into db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot save block audit into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
} }
} }
} }
public async $getBlockPredictionsHistory(div: number, interval: string | null): Promise<any> { public async $getBlocksHealthHistory(div: number, interval: string | null): Promise<any> {
try { try {
let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`; let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`;
@@ -32,17 +31,17 @@ class BlocksAuditRepositories {
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
return rows; return rows;
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot fetch blocks health history. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
public async $getPredictionsCount(): Promise<number> { public async $getBlocksHealthCount(): Promise<number> {
try { try {
const [rows] = await DB.query(`SELECT count(hash) as count FROM blocks_audits`); const [rows] = await DB.query(`SELECT count(hash) as count FROM blocks_audits`);
return rows[0].count; return rows[0].count;
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot fetch blocks health count. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -52,9 +51,10 @@ class BlocksAuditRepositories {
const [rows]: any[] = await DB.query( const [rows]: any[] = await DB.query(
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size, `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
blocks.weight, blocks.tx_count, blocks.weight, blocks.tx_count,
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, match_rate as matchRate transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, sigop_txs as sigopTxs, accelerated_txs as acceleratedTxs, match_rate as matchRate
FROM blocks_audits FROM blocks_audits
JOIN blocks ON blocks.hash = blocks_audits.hash JOIN blocks ON blocks.hash = blocks_audits.hash
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
WHERE blocks_audits.hash = "${hash}" WHERE blocks_audits.hash = "${hash}"
`); `);
@@ -63,6 +63,8 @@ class BlocksAuditRepositories {
rows[0].missingTxs = JSON.parse(rows[0].missingTxs); rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].freshTxs = JSON.parse(rows[0].freshTxs); rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
rows[0].transactions = JSON.parse(rows[0].transactions); rows[0].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template); rows[0].template = JSON.parse(rows[0].template);

View File

@@ -1,4 +1,4 @@
import { BlockExtended, BlockExtension, BlockPrice } from '../mempool.interfaces'; import { BlockExtended, BlockExtension, BlockPrice, EffectiveFeeStats } from '../mempool.interfaces';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { Common } from '../api/common'; import { Common } from '../api/common';
@@ -13,6 +13,48 @@ import chainTips from '../api/chain-tips';
import blocks from '../api/blocks'; import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository'; import BlocksAuditsRepository from './BlocksAuditsRepository';
interface DatabaseBlock {
id: string;
height: number;
version: number;
timestamp: number;
bits: number;
nonce: number;
difficulty: number;
merkle_root: string;
tx_count: number;
size: number;
weight: number;
previousblockhash: string;
mediantime: number;
totalFees: number;
medianFee: number;
feeRange: string;
reward: number;
poolId: number;
poolName: string;
poolSlug: string;
avgFee: number;
avgFeeRate: number;
coinbaseRaw: string;
coinbaseAddress: string;
coinbaseSignature: string;
coinbaseSignatureAscii: string;
avgTxSize: number;
totalInputs: number;
totalOutputs: number;
totalOutputAmt: number;
medianFeeAmt: number;
feePercentiles: string;
segwitTotalTxs: number;
segwitTotalSize: number;
segwitTotalWeight: number;
header: string;
utxoSetChange: number;
utxoSetSize: number;
totalInputAmt: number;
}
const BLOCK_DB_FIELDS = ` const BLOCK_DB_FIELDS = `
blocks.hash AS id, blocks.hash AS id,
blocks.height, blocks.height,
@@ -52,7 +94,7 @@ const BLOCK_DB_FIELDS = `
blocks.header, blocks.header,
blocks.utxoset_change AS utxoSetChange, blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize, blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmts blocks.total_input_amt AS totalInputAmt
`; `;
class BlocksRepository { class BlocksRepository {
@@ -171,6 +213,32 @@ class BlocksRepository {
} }
} }
/**
* Update missing fee amounts fields
*
* @param blockHash
* @param feeAmtPercentiles
* @param medianFeeAmt
*/
public async $updateFeeAmounts(blockHash: string, feeAmtPercentiles, medianFeeAmt) : Promise<void> {
try {
const query = `
UPDATE blocks
SET fee_percentiles = ?, median_fee_amt = ?
WHERE hash = ?
`;
const params: any[] = [
JSON.stringify(feeAmtPercentiles),
medianFeeAmt,
blockHash
];
await DB.query(query, params);
} catch (e: any) {
logger.err(`Cannot update fee amounts for block ${blockHash}. Reason: ' + ${e instanceof Error ? e.message : e}`);
throw e;
}
}
/** /**
* Get all block height that have not been indexed between [startHeight, endHeight] * Get all block height that have not been indexed between [startHeight, endHeight]
*/ */
@@ -330,6 +398,55 @@ class BlocksRepository {
} }
} }
/**
* Get average block health for all blocks for a single pool
*/
public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number> {
const params: any[] = [];
const query = `
SELECT AVG(blocks_audits.match_rate) AS avg_match_rate
FROM blocks
JOIN blocks_audits ON blocks.height = blocks_audits.height
WHERE blocks.pool_id = ?
`;
params.push(poolId);
try {
const [rows] = await DB.query(query, params);
if (!rows[0] || !rows[0].avg_match_rate) {
return 0;
}
return Math.round(rows[0].avg_match_rate * 100) / 100;
} catch (e) {
logger.err(`Cannot get average block health for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get average block health for all blocks for a single pool
*/
public async $getTotalRewardForPoolId(poolId: number): Promise<number> {
const params: any[] = [];
const query = `
SELECT sum(reward) as total_reward
FROM blocks
WHERE blocks.pool_id = ?
`;
params.push(poolId);
try {
const [rows] = await DB.query(query, params);
if (!rows[0] || !rows[0].total_reward) {
return 0;
}
return rows[0].total_reward;
} catch (e) {
logger.err(`Cannot get total reward for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/** /**
* Get the oldest indexed block * Get the oldest indexed block
*/ */
@@ -383,7 +500,7 @@ class BlocksRepository {
const blocks: BlockExtended[] = []; const blocks: BlockExtended[] = [];
for (const block of rows) { for (const block of rows) {
blocks.push(await this.formatDbBlockIntoExtendedBlock(block)); blocks.push(await this.formatDbBlockIntoExtendedBlock(block as DatabaseBlock));
} }
return blocks; return blocks;
@@ -410,37 +527,13 @@ class BlocksRepository {
return null; return null;
} }
return await this.formatDbBlockIntoExtendedBlock(rows[0]); return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);
} catch (e) { } catch (e) {
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
/**
* Get one block by hash
*/
public async $getBlockByHash(hash: string): Promise<object | null> {
try {
const query = `
SELECT ${BLOCK_DB_FIELDS}
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE hash = ?;
`;
const [rows]: any[] = await DB.query(query, [hash]);
if (rows.length <= 0) {
return null;
}
return await this.formatDbBlockIntoExtendedBlock(rows[0]);
} catch (e) {
logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/** /**
* Return blocks difficulty * Return blocks difficulty
*/ */
@@ -550,7 +643,6 @@ class BlocksRepository {
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) { if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`); logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`);
await this.$deleteBlocksFrom(blocks[idx - 1].height); await this.$deleteBlocksFrom(blocks[idx - 1].height);
await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800); await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height); await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height);
return false; return false;
@@ -570,7 +662,7 @@ class BlocksRepository {
* Delete blocks from the database from blockHeight * Delete blocks from the database from blockHeight
*/ */
public async $deleteBlocksFrom(blockHeight: number) { public async $deleteBlocksFrom(blockHeight: number) {
logger.info(`Delete newer blocks from height ${blockHeight} from the database`); logger.info(`Delete newer blocks from height ${blockHeight} from the database`, logger.tags.mining);
try { try {
await DB.query(`DELETE FROM blocks where height >= ${blockHeight}`); await DB.query(`DELETE FROM blocks where height >= ${blockHeight}`);
@@ -748,6 +840,7 @@ class BlocksRepository {
SELECT height SELECT height
FROM compact_cpfp_clusters FROM compact_cpfp_clusters
WHERE height <= ? AND height >= ? WHERE height <= ? AND height >= ?
GROUP BY height
ORDER BY height DESC; ORDER BY height DESC;
`, [currentBlockHeight, minHeight]); `, [currentBlockHeight, minHeight]);
@@ -858,13 +951,32 @@ class BlocksRepository {
} }
} }
/**
* Save indexed effective fee statistics
*
* @param id
* @param feeStats
*/
public async $saveEffectiveFeeStats(id: string, feeStats: EffectiveFeeStats): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET median_fee = ?, fee_span = ?
WHERE hash = ?`,
[feeStats.medianFee, JSON.stringify(feeStats.feeRange), id]
);
} catch (e) {
logger.err(`Cannot update block fee stats. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/** /**
* Convert a mysql row block into a BlockExtended. Note that you * Convert a mysql row block into a BlockExtended. Note that you
* must provide the correct field into dbBlk object param * must provide the correct field into dbBlk object param
* *
* @param dbBlk * @param dbBlk
*/ */
private async formatDbBlockIntoExtendedBlock(dbBlk: any): Promise<BlockExtended> { private async formatDbBlockIntoExtendedBlock(dbBlk: DatabaseBlock): Promise<BlockExtended> {
const blk: Partial<BlockExtended> = {}; const blk: Partial<BlockExtended> = {};
const extras: Partial<BlockExtension> = {}; const extras: Partial<BlockExtension> = {};
@@ -928,6 +1040,7 @@ class BlocksRepository {
} }
// If we're missing block summary related field, check if we can populate them on the fly now // If we're missing block summary related field, check if we can populate them on the fly now
// This is for example triggered upon re-org
if (Common.blocksSummariesIndexingEnabled() && if (Common.blocksSummariesIndexingEnabled() &&
(extras.medianFeeAmt === null || extras.feePercentiles === null)) (extras.medianFeeAmt === null || extras.feePercentiles === null))
{ {
@@ -935,11 +1048,12 @@ class BlocksRepository {
if (extras.feePercentiles === null) { if (extras.feePercentiles === null) {
const block = await bitcoinClient.getBlock(dbBlk.id, 2); const block = await bitcoinClient.getBlock(dbBlk.id, 2);
const summary = blocks.summarizeBlock(block); const summary = blocks.summarizeBlock(block);
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary }); await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions);
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id); extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
} }
if (extras.feePercentiles !== null) { if (extras.feePercentiles !== null) {
extras.medianFeeAmt = extras.feePercentiles[3]; extras.medianFeeAmt = extras.feePercentiles[3];
await this.$updateFeeAmounts(dbBlk.id, extras.feePercentiles, extras.medianFeeAmt);
} }
} }

View File

@@ -1,6 +1,6 @@
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { BlockSummary } from '../mempool.interfaces'; import { BlockSummary, TransactionStripped } from '../mempool.interfaces';
class BlocksSummariesRepository { class BlocksSummariesRepository {
public async $getByBlockId(id: string): Promise<BlockSummary | undefined> { public async $getByBlockId(id: string): Promise<BlockSummary | undefined> {
@@ -17,23 +17,17 @@ class BlocksSummariesRepository {
return undefined; return undefined;
} }
public async $saveSummary(params: { height: number, mined?: BlockSummary}) { public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionStripped[]): Promise<void> {
const blockId = params.mined?.id;
try { try {
const transactions = JSON.stringify(params.mined?.transactions || []); const transactionsStr = JSON.stringify(transactions);
await DB.query(` await DB.query(`
INSERT INTO blocks_summaries (height, id, transactions, template) INSERT INTO blocks_summaries
VALUE (?, ?, ?, ?) SET height = ?, transactions = ?, id = ?
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE transactions = ?`,
transactions = ? [blockHeight, transactionsStr, blockId, transactionsStr]);
`, [params.height, blockId, transactions, '[]', transactions]);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart logger.debug(`Cannot save block summary transactions for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
logger.debug(`Cannot save block summary for ${blockId} because it has already been indexed, ignoring`); throw e;
} else {
logger.debug(`Cannot save block summary for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
} }
} }
@@ -42,17 +36,16 @@ class BlocksSummariesRepository {
try { try {
const transactions = JSON.stringify(params.template?.transactions || []); const transactions = JSON.stringify(params.template?.transactions || []);
await DB.query(` await DB.query(`
INSERT INTO blocks_summaries (height, id, transactions, template) INSERT INTO blocks_templates (id, template)
VALUE (?, ?, ?, ?) VALUE (?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
template = ? template = ?
`, [params.height, blockId, '[]', transactions, transactions]); `, [blockId, transactions, transactions]);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block template for ${blockId} because it has already been indexed, ignoring`); logger.debug(`Cannot save block template for ${blockId} because it has already been indexed, ignoring`);
} else { } else {
logger.debug(`Cannot save block template for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`); logger.warn(`Cannot save block template for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
} }
} }
} }
@@ -68,19 +61,6 @@ class BlocksSummariesRepository {
return []; return [];
} }
/**
* Delete blocks from the database from blockHeight
*/
public async $deleteBlocksFrom(blockHeight: number) {
logger.info(`Delete newer blocks summary from height ${blockHeight} from the database`);
try {
await DB.query(`DELETE FROM blocks_summaries where height >= ${blockHeight}`);
} catch (e) {
logger.err('Cannot delete indexed blocks summaries. Reason: ' + (e instanceof Error ? e.message : e));
}
}
/** /**
* Get the fee percentiles if the block has already been indexed, [] otherwise * Get the fee percentiles if the block has already been indexed, [] otherwise
* *

View File

@@ -48,7 +48,7 @@ class CpfpRepository {
} }
} }
public async $batchSaveClusters(clusters: { root: string, height: number, txs: any, effectiveFeePerVsize: number}[]): Promise<boolean> { public async $batchSaveClusters(clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]): Promise<boolean> {
try { try {
const clusterValues: any[] = []; const clusterValues: any[] = [];
const txs: any[] = []; const txs: any[] = [];

View File

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

View File

@@ -25,7 +25,7 @@ class HashratesRepository {
try { try {
await DB.query(query); await DB.query(query);
} catch (e: any) { } catch (e: any) {
logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -51,7 +51,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -78,7 +78,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -93,7 +93,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows.map(row => row.timestamp); return rows.map(row => row.timestamp);
} catch (e) { } catch (e) {
logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -128,7 +128,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -158,7 +158,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [pool.id]); const [rows]: any[] = await DB.query(query, [pool.id]);
boundaries = rows[0]; boundaries = rows[0];
} catch (e) { } catch (e) {
logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
// Get hashrates entries between boundaries // Get hashrates entries between boundaries
@@ -173,7 +173,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]); const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -192,7 +192,7 @@ class HashratesRepository {
} }
return rows[0]['number']; return rows[0]['number'];
} catch (e) { } catch (e) {
logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@@ -201,7 +201,7 @@ class HashratesRepository {
* Delete most recent data points for re-indexing * Delete most recent data points for re-indexing
*/ */
public async $deleteLastEntries() { public async $deleteLastEntries() {
logger.info(`Delete latest hashrates data points from the database`); logger.info(`Delete latest hashrates data points from the database`, logger.tags.mining);
try { try {
const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`); const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`);
@@ -212,7 +212,7 @@ class HashratesRepository {
mining.lastHashrateIndexingDate = null; mining.lastHashrateIndexingDate = null;
mining.lastWeeklyHashrateIndexingDate = null; mining.lastWeeklyHashrateIndexingDate = null;
} catch (e) { } catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
} }
@@ -220,7 +220,7 @@ class HashratesRepository {
* Delete hashrates from the database from timestamp * Delete hashrates from the database from timestamp
*/ */
public async $deleteHashratesFromTimestamp(timestamp: number) { public async $deleteHashratesFromTimestamp(timestamp: number) {
logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`); logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`, logger.tags.mining);
try { try {
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]); await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]);
@@ -228,7 +228,7 @@ class HashratesRepository {
mining.lastHashrateIndexingDate = null; mining.lastHashrateIndexingDate = null;
mining.lastWeeklyHashrateIndexingDate = null; mining.lastWeeklyHashrateIndexingDate = null;
} catch (e) { } catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
} }
} }

View File

@@ -1,6 +1,5 @@
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
export interface ApiPrice { export interface ApiPrice {
@@ -13,6 +12,16 @@ export interface ApiPrice {
AUD: number, AUD: number,
JPY: number, JPY: number,
} }
const ApiPriceFields = `
UNIX_TIMESTAMP(time) as time,
USD,
EUR,
GBP,
CAD,
CHF,
AUD,
JPY
`;
export interface ExchangeRates { export interface ExchangeRates {
USDEUR: number, USDEUR: number,
@@ -39,7 +48,7 @@ export const MAX_PRICES = {
}; };
class PricesRepository { class PricesRepository {
public async $savePrices(time: number, prices: IConversionRates): Promise<void> { public async $savePrices(time: number, prices: ApiPrice): Promise<void> {
if (prices.USD === -1) { if (prices.USD === -1) {
// Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues // Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues
// As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine // As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine
@@ -60,77 +69,115 @@ class PricesRepository {
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`, VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY] [time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
); );
} catch (e: any) { } catch (e) {
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
public async $getOldestPriceTime(): Promise<number> { public async $getOldestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time LIMIT 1`); const [oldestRow] = await DB.query(`
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
ORDER BY time
LIMIT 1
`);
return oldestRow[0] ? oldestRow[0].time : 0; return oldestRow[0] ? oldestRow[0].time : 0;
} }
public async $getLatestPriceId(): Promise<number | null> { public async $getLatestPriceId(): Promise<number | null> {
const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`); const [oldestRow] = await DB.query(`
return oldestRow[0] ? oldestRow[0].id : null; SELECT id
}
public async $getLatestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
return oldestRow[0] ? oldestRow[0].time : 0;
}
public async $getPricesTimes(): Promise<number[]> {
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time`);
return times.map(time => time.time);
}
public async $getPricesTimesAndId(): Promise<number[]> {
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time, id, USD from prices ORDER BY time`);
return times;
}
public async $getLatestConversionRates(): Promise<any> {
const [rates]: any[] = await DB.query(`
SELECT USD, EUR, GBP, CAD, CHF, AUD, JPY
FROM prices FROM prices
ORDER BY time DESC ORDER BY time DESC
LIMIT 1` LIMIT 1`
); );
if (!rates || rates.length === 0) { return oldestRow[0] ? oldestRow[0].id : null;
}
public async $getLatestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
ORDER BY time DESC
LIMIT 1`
);
return oldestRow[0] ? oldestRow[0].time : 0;
}
public async $getPricesTimes(): Promise<number[]> {
const [times] = await DB.query(`
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
WHERE USD != -1
ORDER BY time
`);
if (!Array.isArray(times)) {
return [];
}
return times.map(time => time.time);
}
public async $getPricesTimesAndId(): Promise<{time: number, id: number, USD: number}[]> {
const [times] = await DB.query(`
SELECT
UNIX_TIMESTAMP(time) AS time,
id,
USD
FROM prices
ORDER BY time
`);
return times as {time: number, id: number, USD: number}[];
}
public async $getLatestConversionRates(): Promise<ApiPrice> {
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
ORDER BY time DESC
LIMIT 1`
);
if (!Array.isArray(rates) || rates.length === 0) {
return priceUpdater.getEmptyPricesObj(); return priceUpdater.getEmptyPricesObj();
} }
return rates[0]; return rates[0] as ApiPrice;
} }
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> { public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
try { try {
const [rates]: any[] = await DB.query(` const [rates] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(time) AS time SELECT ${ApiPriceFields}
FROM prices FROM prices
WHERE UNIX_TIMESTAMP(time) < ? WHERE UNIX_TIMESTAMP(time) < ?
ORDER BY time DESC ORDER BY time DESC
LIMIT 1`, LIMIT 1`,
[timestamp] [timestamp]
); );
if (!rates) { if (!Array.isArray(rates)) {
throw Error(`Cannot get single historical price from the database`); throw Error(`Cannot get single historical price from the database`);
} }
// Compute fiat exchange rates // Compute fiat exchange rates
const latestPrice = await this.$getLatestConversionRates(); let latestPrice = rates[0] as ApiPrice;
if (!latestPrice || latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj();
}
const computeFx = (usd: number, other: number): number =>
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
const exchangeRates: ExchangeRates = { const exchangeRates: ExchangeRates = {
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100, USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100, USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100, USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100, USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100, USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100, USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
}; };
return { return {
prices: rates, prices: rates as ApiPrice[],
exchangeRates: exchangeRates exchangeRates: exchangeRates
}; };
} catch (e) { } catch (e) {
@@ -141,28 +188,35 @@ class PricesRepository {
public async $getHistoricalPrices(): Promise<Conversion | null> { public async $getHistoricalPrices(): Promise<Conversion | null> {
try { try {
const [rates]: any[] = await DB.query(` const [rates] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(time) AS time SELECT ${ApiPriceFields}
FROM prices FROM prices
ORDER BY time DESC ORDER BY time DESC
`); `);
if (!rates) { if (!Array.isArray(rates)) {
throw Error(`Cannot get average historical price from the database`); throw Error(`Cannot get average historical price from the database`);
} }
// Compute fiat exchange rates // Compute fiat exchange rates
const latestPrice: ApiPrice = rates[0]; let latestPrice = rates[0] as ApiPrice;
if (latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj();
}
const computeFx = (usd: number, other: number): number =>
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
const exchangeRates: ExchangeRates = { const exchangeRates: ExchangeRates = {
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100, USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100, USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100, USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100, USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100, USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100, USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
}; };
return { return {
prices: rates, prices: rates as ApiPrice[],
exchangeRates: exchangeRates exchangeRates: exchangeRates
}; };
} catch (e) { } catch (e) {

View File

@@ -27,7 +27,7 @@ class ForensicsService {
private async $runTasks(): Promise<void> { private async $runTasks(): Promise<void> {
try { try {
logger.info(`Running forensics scans`); logger.debug(`Running forensics scans`);
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
await this.$runClosedChannelsForensics(false); await this.$runClosedChannelsForensics(false);
@@ -73,7 +73,7 @@ class ForensicsService {
let progress = 0; let progress = 0;
try { try {
logger.info(`Started running closed channel forensics...`); logger.debug(`Started running closed channel forensics...`);
let channels; let channels;
if (onlyNewChannels) { if (onlyNewChannels) {
channels = await channelsApi.$getClosedChannelsWithoutReason(); channels = await channelsApi.$getClosedChannelsWithoutReason();
@@ -152,11 +152,11 @@ class ForensicsService {
++progress; ++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) { if (elapsedSeconds > 10) {
logger.info(`Updating channel closed channel forensics ${progress}/${channels.length}`); logger.debug(`Updating channel closed channel forensics ${progress}/${channels.length}`);
this.loggerTimer = new Date().getTime() / 1000; this.loggerTimer = new Date().getTime() / 1000;
} }
} }
logger.info(`Closed channels forensics scan complete.`); logger.debug(`Closed channels forensics scan complete.`);
} catch (e) { } catch (e) {
logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e)); logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
} }
@@ -217,7 +217,7 @@ class ForensicsService {
let progress = 0; let progress = 0;
try { try {
logger.info(`Started running open channel forensics...`); logger.debug(`Started running open channel forensics...`);
const channels = await channelsApi.$getChannelsWithoutSourceChecked(); const channels = await channelsApi.$getChannelsWithoutSourceChecked();
for (const openChannel of channels) { for (const openChannel of channels) {
@@ -257,7 +257,7 @@ class ForensicsService {
++progress; ++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) { if (elapsedSeconds > 10) {
logger.info(`Updating opened channel forensics ${progress}/${channels?.length}`); logger.debug(`Updating opened channel forensics ${progress}/${channels?.length}`);
this.loggerTimer = new Date().getTime() / 1000; this.loggerTimer = new Date().getTime() / 1000;
this.truncateTempCache(); this.truncateTempCache();
} }
@@ -266,7 +266,7 @@ class ForensicsService {
} }
} }
logger.info(`Open channels forensics scan complete.`); logger.debug(`Open channels forensics scan complete.`);
} catch (e) { } catch (e) {
logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e)); logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
} finally { } finally {

View File

@@ -283,7 +283,7 @@ class NetworkSyncService {
} else { } else {
log += ` for the first time`; log += ` for the first time`;
} }
logger.info(`${log}`, logger.tags.ln); logger.debug(`${log}`, logger.tags.ln);
const channels = await channelsApi.$getChannelsByStatus([0, 1]); const channels = await channelsApi.$getChannelsByStatus([0, 1]);
for (const channel of channels) { for (const channel of channels) {
@@ -300,7 +300,7 @@ class NetworkSyncService {
++progress; ++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
logger.info(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln); logger.debug(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln);
this.loggerTimer = new Date().getTime() / 1000; this.loggerTimer = new Date().getTime() / 1000;
} }
} }

View File

@@ -22,12 +22,15 @@ class LightningStatsUpdater {
* Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds * Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds
*/ */
private async $logStatsDaily(): Promise<void> { private async $logStatsDaily(): Promise<void> {
const date = new Date(); try {
Common.setDateMidnight(date); const date = new Date();
const networkGraph = await lightningApi.$getNetworkGraph(); Common.setDateMidnight(date);
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph); const networkGraph = await lightningApi.$getNetworkGraph();
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
logger.debug(`Updated latest network stats`, logger.tags.ln); logger.debug(`Updated latest network stats`, logger.tags.ln);
} catch (e) {
logger.err(`Exception in $logStatsDaily. Reason: ${(e instanceof Error ? e.message : e)}`);
}
} }
} }

View File

@@ -15,16 +15,20 @@ class LightningStatsImporter {
topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER; topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER;
async $run(): Promise<void> { async $run(): Promise<void> {
const [channels]: any[] = await DB.query('SELECT short_id from channels;'); try {
logger.info(`Caching funding txs for currently existing channels`, logger.tags.ln); const [channels]: any[] = await DB.query('SELECT short_id from channels;');
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); logger.info(`Caching funding txs for currently existing channels`, logger.tags.ln);
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) { if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
return; return;
}
await this.$importHistoricalLightningStats();
await this.$cleanupIncorrectSnapshot();
} catch (e) {
logger.err(`Exception in LightningStatsImporter::$run(). ${e}`);
} }
await this.$importHistoricalLightningStats();
await this.$cleanupIncorrectSnapshot();
} }
/** /**
@@ -411,7 +415,7 @@ class LightningStatsImporter {
} }
if (totalProcessed > 0) { if (totalProcessed > 0) {
logger.notice(`Lightning network stats historical import completed`, logger.tags.ln); logger.info(`Lightning network stats historical import completed`, logger.tags.ln);
} }
} catch (e) { } catch (e) {
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln); logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln);

View File

@@ -12,12 +12,14 @@ import * as https from 'https';
*/ */
class PoolsUpdater { class PoolsUpdater {
lastRun: number = 0; lastRun: number = 0;
currentSha: string | undefined = undefined; currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
public async updatePoolsJson(): Promise<void> { public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false
) {
return; return;
} }
@@ -33,7 +35,7 @@ class PoolsUpdater {
try { try {
const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github
if (githubSha === undefined) { if (githubSha === null) {
return; return;
} }
@@ -42,12 +44,12 @@ class PoolsUpdater {
} }
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`); logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
if (this.currentSha !== undefined && this.currentSha === githubSha) { if (this.currentSha !== null && this.currentSha === githubSha) {
return; return;
} }
// See backend README for more details about the mining pools update process // See backend README for more details about the mining pools update process
if (this.currentSha !== undefined && // If we don't have any mining pool, download it at least once if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool !process.env.npm_config_update_pools // We're not manually updating mining pool
) { ) {
@@ -57,10 +59,10 @@ class PoolsUpdater {
} }
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === undefined) { if (this.currentSha === null) {
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining); logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
} else { } else {
logger.warn(`pools-v2.json is outdated, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining); logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
} }
const poolsJson = await this.query(this.poolsUrl); const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) { if (poolsJson === undefined) {
@@ -82,7 +84,7 @@ class PoolsUpdater {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining); logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
await DB.query('ROLLBACK;'); await DB.query('ROLLBACK;');
} }
logger.notice('PoolsUpdater completed'); logger.info('PoolsUpdater completed');
} catch (e) { } catch (e) {
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
@@ -108,20 +110,20 @@ class PoolsUpdater {
/** /**
* Fetch our latest pools-v2.json sha from the db * Fetch our latest pools-v2.json sha from the db
*/ */
private async getShaFromDb(): Promise<string | undefined> { private async getShaFromDb(): Promise<string | null> {
try { try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : undefined); return (rows.length > 0 ? rows[0].string : null);
} catch (e) { } catch (e) {
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
return undefined; return null;
} }
} }
/** /**
* Fetch our latest pools-v2.json sha from github * Fetch our latest pools-v2.json sha from github
*/ */
private async fetchPoolsSha(): Promise<string | undefined> { private async fetchPoolsSha(): Promise<string | null> {
const response = await this.query(this.treeUrl); const response = await this.query(this.treeUrl);
if (response !== undefined) { if (response !== undefined) {
@@ -133,7 +135,7 @@ class PoolsUpdater {
} }
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining); logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
return undefined; return null;
} }
/** /**

View File

@@ -8,9 +8,6 @@ class BitfinexApi implements PriceFeed {
public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC'; public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC';
public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist'; public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist';
constructor() {
}
public async $fetchPrice(currency): Promise<number> { public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency); const response = await query(this.url + currency);
if (response && response['last_price']) { if (response && response['last_price']) {

View File

@@ -98,7 +98,7 @@ class KrakenApi implements PriceFeed {
} }
if (Object.keys(priceHistory).length > 0) { if (Object.keys(priceHistory).length > 0) {
logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining); logger.info(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining);
} }
} }
} }

View File

@@ -2,8 +2,7 @@ import * as fs from 'fs';
import path from 'path'; import path from 'path';
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces'; import PricesRepository, { ApiPrice, MAX_PRICES } from '../repositories/PricesRepository';
import PricesRepository, { MAX_PRICES } from '../repositories/PricesRepository';
import BitfinexApi from './price-feeds/bitfinex-api'; import BitfinexApi from './price-feeds/bitfinex-api';
import BitflyerApi from './price-feeds/bitflyer-api'; import BitflyerApi from './price-feeds/bitflyer-api';
import CoinbaseApi from './price-feeds/coinbase-api'; import CoinbaseApi from './price-feeds/coinbase-api';
@@ -21,18 +20,18 @@ export interface PriceFeed {
} }
export interface PriceHistory { export interface PriceHistory {
[timestamp: number]: IConversionRates; [timestamp: number]: ApiPrice;
} }
class PriceUpdater { class PriceUpdater {
public historyInserted = false; public historyInserted = false;
lastRun = 0; private lastRun = 0;
lastHistoricalRun = 0; private lastHistoricalRun = 0;
running = false; private running = false;
feeds: PriceFeed[] = []; private feeds: PriceFeed[] = [];
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY']; private currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
latestPrices: IConversionRates; private latestPrices: ApiPrice;
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined; private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
constructor() { constructor() {
this.latestPrices = this.getEmptyPricesObj(); this.latestPrices = this.getEmptyPricesObj();
@@ -44,8 +43,13 @@ class PriceUpdater {
this.feeds.push(new GeminiApi()); this.feeds.push(new GeminiApi());
} }
public getEmptyPricesObj(): IConversionRates { public getLatestPrices(): ApiPrice {
return this.latestPrices;
}
public getEmptyPricesObj(): ApiPrice {
return { return {
time: 0,
USD: -1, USD: -1,
EUR: -1, EUR: -1,
GBP: -1, GBP: -1,
@@ -56,7 +60,7 @@ class PriceUpdater {
}; };
} }
public setRatesChangedCallback(fn: (rates: IConversionRates) => void) { public setRatesChangedCallback(fn: (rates: ApiPrice) => void): void {
this.ratesChangedCallback = fn; this.ratesChangedCallback = fn;
} }
@@ -69,6 +73,11 @@ class PriceUpdater {
} }
public async $run(): Promise<void> { public async $run(): Promise<void> {
if (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet') {
// Coins have no value on testnet/signet, so we want to always show 0
return;
}
if (this.running === true) { if (this.running === true) {
return; return;
} }
@@ -84,7 +93,7 @@ class PriceUpdater {
if (this.historyInserted === false && config.DATABASE.ENABLED === true) { if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
await this.$insertHistoricalPrices(); await this.$insertHistoricalPrices();
} }
} catch (e) { } catch (e: any) {
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
} }
@@ -156,6 +165,10 @@ class PriceUpdater {
} }
this.lastRun = new Date().getTime() / 1000; this.lastRun = new Date().getTime() / 1000;
if (this.latestPrices.USD === -1) {
this.latestPrices = await PricesRepository.$getLatestConversionRates();
}
} }
/** /**
@@ -209,7 +222,7 @@ class PriceUpdater {
private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise<void> { private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise<void> {
const existingPriceTimes = await PricesRepository.$getPricesTimes(); const existingPriceTimes = await PricesRepository.$getPricesTimes();
logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database`, logger.tags.mining); logger.debug(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database`, logger.tags.mining);
const historicalPrices: PriceHistory[] = []; const historicalPrices: PriceHistory[] = [];
@@ -224,7 +237,7 @@ class PriceUpdater {
// Group them by timestamp and currency, for example // Group them by timestamp and currency, for example
// grouped[123456789]['USD'] = [1, 2, 3, 4]; // grouped[123456789]['USD'] = [1, 2, 3, 4];
const grouped: any = {}; const grouped = {};
for (const historicalEntry of historicalPrices) { for (const historicalEntry of historicalPrices) {
for (const time in historicalEntry) { for (const time in historicalEntry) {
if (existingPriceTimes.includes(parseInt(time, 10))) { if (existingPriceTimes.includes(parseInt(time, 10))) {
@@ -249,7 +262,7 @@ class PriceUpdater {
// Average prices and insert everything into the db // Average prices and insert everything into the db
let totalInserted = 0; let totalInserted = 0;
for (const time in grouped) { for (const time in grouped) {
const prices: IConversionRates = this.getEmptyPricesObj(); const prices: ApiPrice = this.getEmptyPricesObj();
for (const currency in grouped[time]) { for (const currency in grouped[time]) {
if (grouped[time][currency].length === 0) { if (grouped[time][currency].length === 0) {
continue; continue;

View File

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

View File

@@ -5,6 +5,7 @@
"types": ["node", "jest"], "types": ["node", "jest"],
"lib": ["es2019", "dom"], "lib": ["es2019", "dom"],
"strict": true, "strict": true,
"skipLibCheck": true,
"noImplicitAny": false, "noImplicitAny": false,
"sourceMap": false, "sourceMap": false,
"outDir": "dist", "outDir": "dist",

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 January 25, 2022.
Signed: vostrnad

View File

@@ -34,6 +34,7 @@ If you want to use different credentials, specify them in the `docker-compose.ym
CORE_RPC_PORT: "8332" CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "customuser" CORE_RPC_USERNAME: "customuser"
CORE_RPC_PASSWORD: "custompassword" CORE_RPC_PASSWORD: "custompassword"
CORE_RPC_TIMEOUT: "60000"
``` ```
The IP address in the example above refers to Docker's default gateway IP address so that the container can hit the `bitcoind` instance running on the host machine. If your setup is different, update it accordingly. The IP address in the example above refers to Docker's default gateway IP address so that the container can hit the `bitcoind` instance running on the host machine. If your setup is different, update it accordingly.
@@ -112,6 +113,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0, "MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6
}, },
``` ```
@@ -143,6 +145,7 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_ADVANCED_GBT_MEMPOOL: "" MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
MEMPOOL_CPFP_INDEXING: "" MEMPOOL_CPFP_INDEXING: ""
MAX_BLOCKS_BULK_QUERY: "" MAX_BLOCKS_BULK_QUERY: ""
DISK_CACHE_BLOCK_INTERVAL: ""
... ...
``` ```
@@ -158,7 +161,8 @@ Corresponding `docker-compose.yml` overrides:
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool" "PASSWORD": "mempool",
"TIMEOUT": 60000
}, },
``` ```
@@ -170,6 +174,7 @@ Corresponding `docker-compose.yml` overrides:
CORE_RPC_PORT: "" CORE_RPC_PORT: ""
CORE_RPC_USERNAME: "" CORE_RPC_USERNAME: ""
CORE_RPC_PASSWORD: "" CORE_RPC_PASSWORD: ""
CORE_RPC_TIMEOUT: 60000
... ...
``` ```
@@ -199,7 +204,9 @@ Corresponding `docker-compose.yml` overrides:
`mempool-config.json`: `mempool-config.json`:
```json ```json
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000" "REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-socket",
"RETRY_UNIX_SOCKET_AFTER": 30000
}, },
``` ```
@@ -208,6 +215,8 @@ Corresponding `docker-compose.yml` overrides:
api: api:
environment: environment:
ESPLORA_REST_API_URL: "" ESPLORA_REST_API_URL: ""
ESPLORA_UNIX_SOCKET_PATH: ""
ESPLORA_RETRY_UNIX_SOCKET_AFTER: ""
... ...
``` ```
@@ -219,7 +228,8 @@ Corresponding `docker-compose.yml` overrides:
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool" "PASSWORD": "mempool",
"TIMEOUT": 60000
}, },
``` ```
@@ -231,6 +241,7 @@ Corresponding `docker-compose.yml` overrides:
SECOND_CORE_RPC_PORT: "" SECOND_CORE_RPC_PORT: ""
SECOND_CORE_RPC_USERNAME: "" SECOND_CORE_RPC_USERNAME: ""
SECOND_CORE_RPC_PASSWORD: "" SECOND_CORE_RPC_PASSWORD: ""
SECOND_CORE_RPC_TIMEOUT: ""
... ...
``` ```
@@ -258,6 +269,7 @@ Corresponding `docker-compose.yml` overrides:
DATABASE_DATABASE: "" DATABASE_DATABASE: ""
DATABASE_USERNAME: "" DATABASE_USERNAME: ""
DATABASE_PASSWORD: "" DATABASE_PASSWORD: ""
DATABASE_TIMEOUT: ""
... ...
``` ```
@@ -403,6 +415,7 @@ Corresponding `docker-compose.yml` overrides:
"TLS_CERT_PATH": "" "TLS_CERT_PATH": ""
"MACAROON_PATH": "" "MACAROON_PATH": ""
"REST_API_URL": "https://localhost:8080" "REST_API_URL": "https://localhost:8080"
"TIMEOUT": 10000
} }
``` ```
@@ -413,6 +426,7 @@ Corresponding `docker-compose.yml` overrides:
LND_TLS_CERT_PATH: "" LND_TLS_CERT_PATH: ""
LND_MACAROON_PATH: "" LND_MACAROON_PATH: ""
LND_REST_API_URL: "https://localhost:8080" LND_REST_API_URL: "https://localhost:8080"
LND_TIMEOUT: 10000
... ...
``` ```
@@ -432,3 +446,26 @@ Corresponding `docker-compose.yml` overrides:
CLIGHTNING_SOCKET: "" CLIGHTNING_SOCKET: ""
... ...
``` ```
<br/>
`mempool-config.json`:
```json
"MAXMIND": {
"ENABLED": true,
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
}
```
Corresponding `docker-compose.yml` overrides:
```yaml
api:
environment:
MAXMIND_ENABLED: true,
MAXMIND_GEOLITE2_CITY: "/backend/GeoIP/GeoLite2-City.mmdb",
MAXMIND_GEOLITE2_ASN": "/backend/GeoIP/GeoLite2-ASN.mmdb",
MAXMIND_GEOIP2_ISP": "/backend/GeoIP/GeoIP2-ISP.mmdb"
...
```

View File

@@ -17,6 +17,7 @@ WORKDIR /backend
RUN chown 1000:1000 ./ RUN chown 1000:1000 ./
COPY --from=builder --chown=1000:1000 /build/package ./package/ COPY --from=builder --chown=1000:1000 /build/package ./package/
COPY --from=builder --chown=1000:1000 /build/GeoIP ./GeoIP/
COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./ COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./
USER 1000 USER 1000

View File

@@ -26,13 +26,17 @@
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__, "ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__, "ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__, "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL__MAX_BLOCKS_BULK_QUERY__ "MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
"PORT": __CORE_RPC_PORT__, "PORT": __CORE_RPC_PORT__,
"USERNAME": "__CORE_RPC_USERNAME__", "USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__" "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -40,13 +44,16 @@
"TLS_ENABLED": __ELECTRUM_TLS_ENABLED__ "TLS_ENABLED": __ELECTRUM_TLS_ENABLED__
}, },
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__" "REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__", "HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": __SECOND_CORE_RPC_PORT__, "PORT": __SECOND_CORE_RPC_PORT__,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__", "USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__" "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__
}, },
"DATABASE": { "DATABASE": {
"ENABLED": __DATABASE_ENABLED__, "ENABLED": __DATABASE_ENABLED__,
@@ -55,7 +62,8 @@
"PORT": __DATABASE_PORT__, "PORT": __DATABASE_PORT__,
"DATABASE": "__DATABASE_DATABASE__", "DATABASE": "__DATABASE_DATABASE__",
"USERNAME": "__DATABASE_USERNAME__", "USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__" "PASSWORD": "__DATABASE_PASSWORD__",
"TIMEOUT": __DATABASE_TIMEOUT__
}, },
"SYSLOG": { "SYSLOG": {
"ENABLED": __SYSLOG_ENABLED__, "ENABLED": __SYSLOG_ENABLED__,
@@ -78,12 +86,15 @@
"STATS_REFRESH_INTERVAL": __LIGHTNING_STATS_REFRESH_INTERVAL__, "STATS_REFRESH_INTERVAL": __LIGHTNING_STATS_REFRESH_INTERVAL__,
"GRAPH_REFRESH_INTERVAL": __LIGHTNING_GRAPH_REFRESH_INTERVAL__, "GRAPH_REFRESH_INTERVAL": __LIGHTNING_GRAPH_REFRESH_INTERVAL__,
"LOGGER_UPDATE_INTERVAL": __LIGHTNING_LOGGER_UPDATE_INTERVAL__, "LOGGER_UPDATE_INTERVAL": __LIGHTNING_LOGGER_UPDATE_INTERVAL__,
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__" "TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
"FORENSICS_INTERVAL": __LIGHTNING_FORENSICS_INTERVAL__,
"FORENSICS_RATE_LIMIT": __LIGHTNING_FORENSICS_RATE_LIMIT__
}, },
"LND": { "LND": {
"TLS_CERT_PATH": "__LND_TLS_CERT_PATH__", "TLS_CERT_PATH": "__LND_TLS_CERT_PATH__",
"MACAROON_PATH": "__LND_MACAROON_PATH__", "MACAROON_PATH": "__LND_MACAROON_PATH__",
"REST_API_URL": "__LND_REST_API_URL__" "REST_API_URL": "__LND_REST_API_URL__",
"TIMEOUT": __LND_TIMEOUT__
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__" "SOCKET": "__CLIGHTNING_SOCKET__"
@@ -107,5 +118,15 @@
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__", "LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__", "BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__" "BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
},
"MAXMIND": {
"ENABLED": __MAXMIND_ENABLED__,
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
},
"MEMPOOL_SERVICES": {
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
} }
} }

View File

@@ -22,7 +22,6 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0} __MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool} __MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false} __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
@@ -31,12 +30,14 @@ __MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false} __MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0} __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
__CORE_RPC_PORT__=${CORE_RPC_PORT:=8332} __CORE_RPC_PORT__=${CORE_RPC_PORT:=8332}
__CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool} __CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool} __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
# ELECTRUM # ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -45,12 +46,15 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
# ESPLORA # ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000} __ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
# SECOND_CORE_RPC # SECOND_CORE_RPC
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1} __SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
__SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332} __SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332}
__SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool} __SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool} __SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
__SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000}
# DATABASE # DATABASE
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true} __DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
@@ -60,6 +64,7 @@ __DATABASE_PORT__=${DATABASE_PORT:=3306}
__DATABASE_DATABASE__=${DATABASE_DATABASE:=mempool} __DATABASE_DATABASE__=${DATABASE_DATABASE:=mempool}
__DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool} __DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool}
__DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool} __DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool}
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
# SYSLOG # SYSLOG
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false} __SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
@@ -103,40 +108,53 @@ __LIGHTNING_TOPOLOGY_FOLDER__=${LIGHTNING_TOPOLOGY_FOLDER:=""}
__LIGHTNING_STATS_REFRESH_INTERVAL__=${LIGHTNING_STATS_REFRESH_INTERVAL:=600} __LIGHTNING_STATS_REFRESH_INTERVAL__=${LIGHTNING_STATS_REFRESH_INTERVAL:=600}
__LIGHTNING_GRAPH_REFRESH_INTERVAL__=${LIGHTNING_GRAPH_REFRESH_INTERVAL:=600} __LIGHTNING_GRAPH_REFRESH_INTERVAL__=${LIGHTNING_GRAPH_REFRESH_INTERVAL:=600}
__LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30} __LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30}
__LIGHTNING_FORENSICS_INTERVAL__=${LIGHTNING_FORENSICS_INTERVAL:=43200}
__LIGHTNING_FORENSICS_RATE_LIMIT__=${LIGHTNING_FORENSICS_RATE_LIMIT:=20}
# LND # LND
__LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""} __LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""}
__LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""} __LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""}
__LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"} __LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
__LND_TIMEOUT__=${LND_TIMEOUT:=10000}
# CLN # CLN
__CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""} __CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""}
# MAXMIND
__MAXMIND_ENABLED__=${MAXMIND_ENABLED:=true}
__MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="/backend/GeoIP/GeoLite2-City.mmdb"}
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"}
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
# MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__==${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_ACCELERATIONS__==${MEMPOOL_SERVICES_ACCELERATIONS:=false}
mkdir -p "${__MEMPOOL_CACHE_DIR__}" mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json sed -i "s!__MEMPOOL_NETWORK__!${__MEMPOOL_NETWORK__}!g" mempool-config.json
sed -i "s/__MEMPOOL_BACKEND__/${__MEMPOOL_BACKEND__}/g" mempool-config.json sed -i "s!__MEMPOOL_BACKEND__!${__MEMPOOL_BACKEND__}!g" mempool-config.json
sed -i "s/__MEMPOOL_ENABLED__/${__MEMPOOL_ENABLED__}/g" mempool-config.json sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!g" mempool-config.json
sed -i "s/__MEMPOOL_HTTP_PORT__/${__MEMPOOL_HTTP_PORT__}/g" mempool-config.json sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json
sed -i "s/__MEMPOOL_SPAWN_CLUSTER_PROCS__/${__MEMPOOL_SPAWN_CLUSTER_PROCS__}/g" mempool-config.json sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
sed -i "s/__MEMPOOL_POLL_RATE_MS__/${__MEMPOOL_POLL_RATE_MS__}/g" mempool-config.json sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
sed -i "s/__MEMPOOL_CLEAR_PROTECTION_MINUTES__/${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}/g" mempool-config.json sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
sed -i "s/__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__/${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}/g" mempool-config.json sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
sed -i "s/__MEMPOOL_BLOCK_WEIGHT_UNITS__/${__MEMPOOL_BLOCK_WEIGHT_UNITS__}/g" mempool-config.json sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
sed -i "s/__MEMPOOL_INITIAL_BLOCKS_AMOUNT__/${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s!__MEMPOOL_INITIAL_BLOCKS_AMOUNT__!${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}!g" mempool-config.json
sed -i "s/__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__/${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s!__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__!${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}!g" mempool-config.json
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s!__MEMPOOL_INDEXING_BLOCKS_AMOUNT__!${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}!g" mempool-config.json
sed -i "s/__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__/${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}/g" mempool-config.json sed -i "s!__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__!${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}!g" mempool-config.json
sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json sed -i "s!__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__!${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}!g" mempool-config.json
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s!__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__!${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}!g" mempool-config.json
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
@@ -144,50 +162,55 @@ sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
sed -i "s/__CORE_RPC_USERNAME__/${__CORE_RPC_USERNAME__}/g" mempool-config.json sed -i "s!__CORE_RPC_USERNAME__!${__CORE_RPC_USERNAME__}!g" mempool-config.json
sed -i "s/__CORE_RPC_PASSWORD__/${__CORE_RPC_PASSWORD__}/g" mempool-config.json sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s/__ELECTRUM_HOST__/${__ELECTRUM_HOST__}/g" mempool-config.json sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s/__ELECTRUM_PORT__/${__ELECTRUM_PORT__}/g" mempool-config.json sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
sed -i "s/__ELECTRUM_TLS_ENABLED__/${__ELECTRUM_TLS_ENABLED__}/g" mempool-config.json sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config.json
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_HOST__/${__SECOND_CORE_RPC_HOST__}/g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_PORT__/${__SECOND_CORE_RPC_PORT__}/g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_USERNAME__/${__SECOND_CORE_RPC_USERNAME__}/g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_USERNAME__!${__SECOND_CORE_RPC_USERNAME__}!g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_PASSWORD__/${__SECOND_CORE_RPC_PASSWORD__}/g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_PASSWORD__!${__SECOND_CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_TIMEOUT__!${__SECOND_CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s/__DATABASE_ENABLED__/${__DATABASE_ENABLED__}/g" mempool-config.json sed -i "s!__DATABASE_ENABLED__!${__DATABASE_ENABLED__}!g" mempool-config.json
sed -i "s/__DATABASE_HOST__/${__DATABASE_HOST__}/g" mempool-config.json sed -i "s!__DATABASE_HOST__!${__DATABASE_HOST__}!g" mempool-config.json
sed -i "s!__DATABASE_SOCKET__!${__DATABASE_SOCKET__}!g" mempool-config.json sed -i "s!__DATABASE_SOCKET__!${__DATABASE_SOCKET__}!g" mempool-config.json
sed -i "s!__DATABASE_PORT__!${__DATABASE_PORT__}!g" mempool-config.json
sed -i "s!__DATABASE_DATABASE__!${__DATABASE_DATABASE__}!g" mempool-config.json
sed -i "s!__DATABASE_USERNAME__!${__DATABASE_USERNAME__}!g" mempool-config.json
sed -i "s!__DATABASE_PASSWORD__!${__DATABASE_PASSWORD__}!g" mempool-config.json
sed -i "s!__DATABASE_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json
sed -i "s/__DATABASE_PORT__/${__DATABASE_PORT__}/g" mempool-config.json sed -i "s!__SYSLOG_ENABLED__!${__SYSLOG_ENABLED__}!g" mempool-config.json
sed -i "s/__DATABASE_DATABASE__/${__DATABASE_DATABASE__}/g" mempool-config.json sed -i "s!__SYSLOG_HOST__!${__SYSLOG_HOST__}!g" mempool-config.json
sed -i "s/__DATABASE_USERNAME__/${__DATABASE_USERNAME__}/g" mempool-config.json sed -i "s!__SYSLOG_PORT__!${__SYSLOG_PORT__}!g" mempool-config.json
sed -i "s/__DATABASE_PASSWORD__/${__DATABASE_PASSWORD__}/g" mempool-config.json sed -i "s!__SYSLOG_MIN_PRIORITY__!${__SYSLOG_MIN_PRIORITY__}!g" mempool-config.json
sed -i "s!__SYSLOG_FACILITY__!${__SYSLOG_FACILITY__}!g" mempool-config.json
sed -i "s/__SYSLOG_ENABLED__/${__SYSLOG_ENABLED__}/g" mempool-config.json sed -i "s!__STATISTICS_ENABLED__!${__STATISTICS_ENABLED__}!g" mempool-config.json
sed -i "s/__SYSLOG_HOST__/${__SYSLOG_HOST__}/g" mempool-config.json sed -i "s!__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__!${__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__}!g" mempool-config.json
sed -i "s/__SYSLOG_PORT__/${__SYSLOG_PORT__}/g" mempool-config.json
sed -i "s/__SYSLOG_MIN_PRIORITY__/${__SYSLOG_MIN_PRIORITY__}/g" mempool-config.json
sed -i "s/__SYSLOG_FACILITY__/${__SYSLOG_FACILITY__}/g" mempool-config.json
sed -i "s/__STATISTICS_ENABLED__/${__STATISTICS_ENABLED__}/g" mempool-config.json sed -i "s!__BISQ_ENABLED__!${__BISQ_ENABLED__}!g" mempool-config.json
sed -i "s/__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__/${__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__}/g" mempool-config.json
sed -i "s/__BISQ_ENABLED__/${__BISQ_ENABLED__}/g" mempool-config.json
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json sed -i "s!__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_ENABLED__!${__SOCKS5PROXY_ENABLED__}!g" mempool-config.json
sed -i "s/__SOCKS5PROXY_USE_ONION__/${__SOCKS5PROXY_USE_ONION__}/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_HOST__!${__SOCKS5PROXY_HOST__}!g" mempool-config.json
sed -i "s/__SOCKS5PROXY_PORT__/${__SOCKS5PROXY_PORT__}/g" mempool-config.json sed -i "s!__SOCKS5PROXY_PORT__!${__SOCKS5PROXY_PORT__}!g" mempool-config.json
sed -i "s/__SOCKS5PROXY_USERNAME__/${__SOCKS5PROXY_USERNAME__}/g" mempool-config.json sed -i "s!__SOCKS5PROXY_USERNAME__!${__SOCKS5PROXY_USERNAME__}!g" mempool-config.json
sed -i "s/__SOCKS5PROXY_PASSWORD__/${__SOCKS5PROXY_PASSWORD__}/g" mempool-config.json sed -i "s!__SOCKS5PROXY_PASSWORD__!${__SOCKS5PROXY_PASSWORD__}!g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json sed -i "s!__PRICE_DATA_SERVER_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!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
@@ -206,13 +229,27 @@ sed -i "s!__LIGHTNING_TOPOLOGY_FOLDER__!${__LIGHTNING_TOPOLOGY_FOLDER__}!g" memp
sed -i "s!__LIGHTNING_STATS_REFRESH_INTERVAL__!${__LIGHTNING_STATS_REFRESH_INTERVAL__}!g" mempool-config.json sed -i "s!__LIGHTNING_STATS_REFRESH_INTERVAL__!${__LIGHTNING_STATS_REFRESH_INTERVAL__}!g" mempool-config.json
sed -i "s!__LIGHTNING_GRAPH_REFRESH_INTERVAL__!${__LIGHTNING_GRAPH_REFRESH_INTERVAL__}!g" mempool-config.json sed -i "s!__LIGHTNING_GRAPH_REFRESH_INTERVAL__!${__LIGHTNING_GRAPH_REFRESH_INTERVAL__}!g" mempool-config.json
sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTERVAL__}!g" mempool-config.json sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTERVAL__}!g" mempool-config.json
sed -i "s!__LIGHTNING_FORENSICS_INTERVAL__!${__LIGHTNING_FORENSICS_INTERVAL__}!g" mempool-config.json
sed -i "s!__LIGHTNING_FORENSICS_RATE_LIMIT__!${__LIGHTNING_FORENSICS_RATE_LIMIT__}!g" mempool-config.json
# LND # LND
sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json
sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json
sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
sed -i "s!__LND_TIMEOUT__!${__LND_TIMEOUT__}!g" mempool-config.json
# CLN # CLN
sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json
# MAXMIND
sed -i "s!__MAXMIND_ENABLED__!${__MAXMIND_ENABLED__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOLITE2_CITY__!${__MAXMIND_GEOLITE2_CITY__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json
# MEMPOOL_SERVICES
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
node /backend/package/index.js node /backend/package/index.js

View File

@@ -10,6 +10,10 @@ cp /etc/nginx/nginx.conf /patch/nginx.conf
sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf
cat /patch/nginx.conf > /etc/nginx/nginx.conf cat /patch/nginx.conf > /etc/nginx/nginx.conf
if [ "${LIGHTNING_DETECTED_PORT}" != "" ];then
export LIGHTNING=true
fi
# Runtime overrides - read env vars defined in docker compose # Runtime overrides - read env vars defined in docker compose
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false} __TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
@@ -35,6 +39,7 @@ __AUDIT__=${AUDIT:=false}
__MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0} __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0} __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
__FULL_RBF_ENABLED__=${FULL_RBF_ENABLED:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
# Export as environment variables to be used by envsubst # Export as environment variables to be used by envsubst
@@ -61,6 +66,7 @@ export __AUDIT__
export __MAINNET_BLOCK_AUDIT_START_HEIGHT__ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__ export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__ export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
export __FULL_RBF_ENABLED__
export __HISTORICAL_PRICE__ export __HISTORICAL_PRICE__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname) folder=$(find /var/www/mempool -name "config.js" | xargs dirname)

View File

@@ -3,6 +3,11 @@
#backend #backend
cp ./docker/backend/* ./backend/ cp ./docker/backend/* ./backend/
#geoip-data
mkdir -p ./backend/GeoIP/
wget -O ./backend/GeoIP/GeoLite2-City.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-City.mmdb
wget -O ./backend/GeoIP/GeoLite2-ASN.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-ASN.mmdb
#frontend #frontend
localhostIP="127.0.0.1" localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend cp ./docker/frontend/* ./frontend

3
frontend/.gitignore vendored
View File

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

View File

@@ -106,13 +106,15 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Arabic @baro0k * Arabic @baro0k
* Czech @pixelmade2 * Czech @pixelmade2
* Danish @pierrevendelboe
* German @Emzy * German @Emzy
* English (default) * English (default)
* Spanish @maxhodler @bisqes * Spanish @maxhodler @bisqes
* Persian @techmix * Persian @techmix
* French @Bayernatoor * French @Bayernatoor
* Korean @kcalvinalvinn * Korean @kcalvinalvinn @sogoagain
* Italian @HodlBits * Italian @HodlBits
* Lithuanian @eimze21
* Hebrew @rapidlab309 * Hebrew @rapidlab309
* Georgian @wyd_idk * Georgian @wyd_idk
* Hungarian @btcdragonlord * Hungarian @btcdragonlord

View File

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

View File

@@ -158,10 +158,10 @@ describe('Liquid', () => {
it('show empty unblinded TX', () => { it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`); cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', ''); cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', ''); cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', ''); cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
}); });
@@ -169,8 +169,8 @@ describe('Liquid', () => {
it('show invalid unblinded TX hex', () => { it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`); cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', ''); cy.get('.table-tx-vin tr').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr').should('have.class', ''); cy.get('.table-tx-vout tr').should('have.class', 'ng-star-inserted');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)'); cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
}); });

View File

@@ -109,10 +109,10 @@ describe('Liquid Testnet', () => {
it('show empty unblinded TX', () => { it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=`); cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', ''); cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', ''); cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', ''); cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
}); });
@@ -120,8 +120,8 @@ describe('Liquid Testnet', () => {
it('show invalid unblinded TX hex', () => { it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/2477f220eef1d03f8ffa4a2861c275d155c3562adf0d79523aeeb0c59ee611ba#blinded=5000`); cy.visit(`${basePath}/tx/2477f220eef1d03f8ffa4a2861c275d155c3562adf0d79523aeeb0c59ee611ba#blinded=5000`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', ''); cy.get('.table-tx-vin tr').should('have.class', 'ng-star-inserted');
cy.get('.table-tx-vout tr').should('have.class', ''); cy.get('.table-tx-vout tr').should('have.class', 'ng-star-inserted');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)'); cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
}); });

View File

@@ -127,7 +127,7 @@ describe('Mainnet', () => {
cy.get('.search-box-container > .form-control').type('S').then(() => { cy.get('.search-box-container > .form-control').type('S').then(() => {
cy.wait('@search-1wizS'); cy.wait('@search-1wizS');
cy.get('app-search-results button.dropdown-item').should('have.length', 5); cy.get('app-search-results button.dropdown-item').should('have.length', 6);
}); });
cy.get('.search-box-container > .form-control').type('A').then(() => { cy.get('.search-box-container > .form-control').type('A').then(() => {
@@ -504,9 +504,17 @@ describe('Mainnet', () => {
describe('RBF transactions', () => { describe('RBF transactions', () => {
it('shows RBF transactions properly (mobile)', () => { it('shows RBF transactions properly (mobile)', () => {
cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', {
fixture: 'mainnet_tx_cached.json'
}).as('cached_tx');
cy.intercept('/api/v1/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5/rbf', {
fixture: 'mainnet_rbf_new.json'
}).as('rbf');
cy.viewport('iphone-xr'); cy.viewport('iphone-xr');
cy.mockMempoolSocket(); cy.mockMempoolSocket();
cy.visit('/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5'); cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
@@ -524,22 +532,30 @@ describe('Mainnet', () => {
} }
}); });
cy.get('.alert-mempool').should('be.visible'); cy.get('.alert').should('be.visible');
cy.get('.alert-mempool').invoke('css', 'width').then((alertWidth) => { cy.get('.alert').invoke('css', 'width').then((alertWidth) => {
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth); cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
}); });
cy.get('.btn-success').then(getRectangle).then((rectA) => { cy.get('.btn-danger').then(getRectangle).then((rectA) => {
cy.get('.alert-mempool').then(getRectangle).then((rectB) => { cy.get('.alert').then(getRectangle).then((rectB) => {
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false; expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
}); });
}); });
}); });
it('shows RBF transactions properly (desktop)', () => { it('shows RBF transactions properly (desktop)', () => {
cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', {
fixture: 'mainnet_tx_cached.json'
}).as('cached_tx');
cy.intercept('/api/v1/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5/rbf', {
fixture: 'mainnet_rbf_new.json'
}).as('rbf');
cy.viewport('macbook-16'); cy.viewport('macbook-16');
cy.mockMempoolSocket(); cy.mockMempoolSocket();
cy.visit('/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5'); cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
@@ -557,17 +573,17 @@ describe('Mainnet', () => {
} }
}); });
cy.get('.alert-mempool').should('be.visible'); cy.get('.alert').should('be.visible');
const alertLocator = '.alert-mempool'; const alertLocator = '.alert';
const tableLocator = '.container-xl > :nth-child(3)'; const tableLocator = '.container-xl > :nth-child(3)';
cy.get(tableLocator).invoke('css', 'width').then((firstWidth) => { cy.get(tableLocator).invoke('css', 'width').then((firstWidth) => {
cy.get(alertLocator).invoke('css', 'width').should('equal', firstWidth); cy.get(alertLocator).invoke('css', 'width').should('equal', firstWidth);
}); });
cy.get('.btn-success').then(getRectangle).then((rectA) => { cy.get('.btn-danger').then(getRectangle).then((rectA) => {
cy.get('.alert-mempool').then(getRectangle).then((rectB) => { cy.get('.alert').then(getRectangle).then((rectB) => {
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false; expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
}); });
}); });

View File

@@ -1,52 +1,4 @@
{ {
"rbfTransaction": { "txReplaced": {
"txid": "8913ec7ba0ede285dbd120e46f6d61a28f2903c10814a6f6c4f97d0edf3e1f46", "txid": "8913ec7ba0ede285dbd120e46f6d61a28f2903c10814a6f6c4f97d0edf3e1f46"
"version": 2, }}
"locktime": 632699,
"vin": [
{
"txid": "02238126a63ea2669c5f378012180ef8b54402a949316f9b2f1352c51730a086",
"vout": 0,
"prevout": {
"scriptpubkey": "a914f8e495456956c833e5e8c69b9a9dc041aa14c72f87",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 f8e495456956c833e5e8c69b9a9dc041aa14c72f OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "3QP3LMD8veT5GtWV83Nosif2Bhr73857VB",
"value": 25000000
},
"scriptsig": "22002043288fbbc0fc5efa86c229dbb7d88ab78d57957c65b5d5ceaece70838976ad1b",
"scriptsig_asm": "OP_PUSHBYTES_34 002043288fbbc0fc5efa86c229dbb7d88ab78d57957c65b5d5ceaece70838976ad1b",
"witness": [
"",
"3044022009e2d3a8e645f65bc89c8492cd9c08e6fb02609fd402214884a754a1970145340220575bb325429def59f3a3f1e22d9740a3feecbe97438ff3bb5796b2c46b3c477f01",
"3044022039c34372882da8fc1c1243bd72b5e7e5e6870301ef56bdebb87bc647fb50f9b5022071a704ee77d742f78b10e45be675d4c45a5f31e884139e75c975144fde70e41701",
"522102346eb7133f11e0dc279bc592d5ac948a91676372a6144c9ae2085625d7fbf70421021b9508a458f9d59be4eb8cc87ad582c3b494106fb1d4ec22801569be0700eb7b52ae"
],
"is_coinbase": false,
"sequence": 4294967293,
"inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_32 43288fbbc0fc5efa86c229dbb7d88ab78d57957c65b5d5ceaece70838976ad1b",
"inner_witnessscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 02346eb7133f11e0dc279bc592d5ac948a91676372a6144c9ae2085625d7fbf704 OP_PUSHBYTES_33 021b9508a458f9d59be4eb8cc87ad582c3b494106fb1d4ec22801569be0700eb7b OP_PUSHNUM_2 OP_CHECKMULTISIG"
}
],
"vout": [
{
"scriptpubkey": "a914fd4e5e59dd5cf2dc48eaedf1a2a1650ca1ce9d7f87",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 fd4e5e59dd5cf2dc48eaedf1a2a1650ca1ce9d7f OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "3QnNmDhZS7toHA7bhhbTPBdtpLJoeecq5c",
"value": 13986350
},
{
"scriptpubkey": "76a914edc93d0446deec1c2d514f3a490f050096e74e0e88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 edc93d0446deec1c2d514f3a490f050096e74e0e OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1NgJDkTUqJxxCAAZrrsC87kWag5kphrRtM",
"value": 11000000
}
],
"size": 372,
"weight": 828,
"fee": 1.5,
"status": { "confirmed": false }
}
}

View File

@@ -0,0 +1,31 @@
{
"replacements": {
"tx": {
"txid": "f22735aaa8eb84bcae3e7705f78609c6f5f0cd7dfc34ae03094e61f2dab0cc64",
"fee": 13843,
"vsize": 109.25,
"value": 253003805,
"rate": 36.04666732302845,
"rbf": true
},
"time": 1683865345,
"fullRbf": false,
"replaces": [
{
"tx": {
"txid": "21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f",
"fee": 8794,
"vsize": 109.25,
"value": 253008854,
"rate": 35.05247612484001,
"rbf": true
},
"time": 1683864993,
"interval": 352,
"fullRbf": false,
"replaces": []
}
]
},
"replaces": null
}

View File

@@ -0,0 +1,60 @@
{
"vsize": 109,
"feePerVsize": 80.49427917620137,
"effectiveFeePerVsize": 35.05247612484001,
"txid": "21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "1e3bd5c634781a6ba8bb3d3385b14739bf38cad5332d5fbc5c0ab775e54b9aef",
"vout": 144,
"prevout": {
"scriptpubkey": "0014d98654186b90d95da7e31a30929f5b5b6a0af250",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 d98654186b90d95da7e31a30929f5b5b6a0af250",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qmxr9gxrtjrv4mflrrgcf986mtd4q4ujss432tk",
"value": 253017648
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"30440220448e8f58fcdea87c1969d58438b49da5b43712380bc4c68b02d22cf6b164907302207b2ed660f1a5b3b74f712961ffb3f3a7d1ac6e48b269ea6ff15df985042211f301",
"02e39a1f3583e382cec1a1fab6a3f5950b6403c953fada58d809127a497f502ebe"
],
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "0014edb5167da7e97c73d7931eb2130ac3e34e6845a9",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 edb5167da7e97c73d7931eb2130ac3e34e6845a9",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qak63vld8a97884unr6epxzkrud8xs3dfdqswy2",
"value": 253008854
}
],
"size": 191,
"weight": 437,
"fee": 8794,
"status": {
"confirmed": false
},
"firstSeen": 1683864993,
"uid": 298353,
"position": {
"block": 0,
"vsize": 886207.5
},
"cpfpChecked": true,
"ancestors": [
{
"txid": "1e3bd5c634781a6ba8bb3d3385b14739bf38cad5332d5fbc5c0ab775e54b9aef",
"fee": 169220,
"weight": 19877
}
],
"descendants": [],
"bestDescendant": null
}

View File

@@ -22,5 +22,6 @@
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0, "TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0, "SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
"LIGHTNING": false, "LIGHTNING": false,
"FULL_RBF_ENABLED": false,
"HISTORICAL_PRICE": true "HISTORICAL_PRICE": true
} }

View File

@@ -1,12 +1,12 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.5.0-dev", "version": "2.6.0-dev",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.5.0-dev", "version": "2.6.0-dev",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "^14.2.10", "@angular-devkit/build-angular": "^14.2.10",

View File

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

View File

@@ -4,6 +4,8 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
import { StartComponent } from './components/start/start.component'; import { StartComponent } from './components/start/start.component';
import { TransactionComponent } from './components/transaction/transaction.component'; import { TransactionComponent } from './components/transaction/transaction.component';
import { BlockComponent } from './components/block/block.component'; import { BlockComponent } from './components/block/block.component';
import { ClockMinedComponent as ClockMinedComponent } from './components/clock/clock-mined.component';
import { ClockMempoolComponent as ClockMempoolComponent } from './components/clock/clock-mempool.component';
import { AddressComponent } from './components/address/address.component'; import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component'; import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component'; import { AboutComponent } from './components/about/about.component';
@@ -14,6 +16,7 @@ import { TrademarkPolicyComponent } from './components/trademark-policy/trademar
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component'; import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { BlocksList } from './components/blocks-list/blocks-list.component'; import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RbfList } from './components/rbf-list/rbf-list.component';
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component'; import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
@@ -56,6 +59,10 @@ let routes: Routes = [
path: 'blocks', path: 'blocks',
component: BlocksList, component: BlocksList,
}, },
{
path: 'rbf',
component: RbfList,
},
{ {
path: 'terms-of-service', path: 'terms-of-service',
component: TermsOfServiceComponent component: TermsOfServiceComponent
@@ -162,6 +169,10 @@ let routes: Routes = [
path: 'blocks', path: 'blocks',
component: BlocksList, component: BlocksList,
}, },
{
path: 'rbf',
component: RbfList,
},
{ {
path: 'terms-of-service', path: 'terms-of-service',
component: TermsOfServiceComponent component: TermsOfServiceComponent
@@ -264,6 +275,10 @@ let routes: Routes = [
path: 'blocks', path: 'blocks',
component: BlocksList, component: BlocksList,
}, },
{
path: 'rbf',
component: RbfList,
},
{ {
path: 'terms-of-service', path: 'terms-of-service',
component: TermsOfServiceComponent component: TermsOfServiceComponent
@@ -342,6 +357,14 @@ let routes: Routes = [
}, },
], ],
}, },
{
path: 'clock-mined',
component: ClockMinedComponent,
},
{
path: 'clock-mempool',
component: ClockMempoolComponent,
},
{ {
path: 'status', path: 'status',
data: { networks: ['bitcoin', 'liquid'] }, data: { networks: ['bitcoin', 'liquid'] },

View File

@@ -29,6 +29,14 @@ export const mempoolFeeColors = [
'ba3243', 'ba3243',
'b92b48', 'b92b48',
'b9254b', 'b9254b',
'b8214d',
'b71d4f',
'b61951',
'b41453',
'b30e55',
'b10857',
'b00259',
'ae005b',
]; ];
export const chartColors = [ export const chartColors = [
@@ -69,6 +77,7 @@ export const chartColors = [
"#3E2723", "#3E2723",
"#212121", "#212121",
"#263238", "#263238",
"#801313",
]; ];
export const poolsColor = { export const poolsColor = {
@@ -87,9 +96,9 @@ export const languages: Language[] = [
{ code: 'ar', name: 'العربية' }, // Arabic { code: 'ar', name: 'العربية' }, // Arabic
// { code: 'bg', name: 'Български' }, // Bulgarian // { code: 'bg', name: 'Български' }, // Bulgarian
// { code: 'bs', name: 'Bosanski' }, // Bosnian // { code: 'bs', name: 'Bosanski' }, // Bosnian
{ code: 'ca', name: 'Català' }, // Catalan // { code: 'ca', name: 'Català' }, // Catalan
{ code: 'cs', name: 'Čeština' }, // Czech { code: 'cs', name: 'Čeština' }, // Czech
// { code: 'da', name: 'Dansk' }, // Danish { code: 'da', name: 'Dansk' }, // Danish
{ code: 'de', name: 'Deutsch' }, // German { code: 'de', name: 'Deutsch' }, // German
// { code: 'et', name: 'Eesti' }, // Estonian // { code: 'et', name: 'Eesti' }, // Estonian
// { code: 'el', name: 'Ελληνικά' }, // Greek // { code: 'el', name: 'Ελληνικά' }, // Greek
@@ -136,13 +145,90 @@ export const languages: Language[] = [
]; ];
export const specialBlocks = { export const specialBlocks = {
'0': {
labelEvent: 'Genesis',
labelEventCompleted: 'The Genesis of Bitcoin',
networks: ['mainnet', 'testnet'],
},
'210000': {
labelEvent: 'Bitcoin\'s 1st Halving',
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
networks: ['mainnet', 'testnet'],
},
'420000': {
labelEvent: 'Bitcoin\'s 2nd Halving',
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
networks: ['mainnet', 'testnet'],
},
'630000': {
labelEvent: 'Bitcoin\'s 3rd Halving',
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
networks: ['mainnet', 'testnet'],
},
'709632': { '709632': {
labelEvent: 'Taproot 🌱 activation', labelEvent: 'Taproot 🌱 activation',
labelEventCompleted: 'Taproot 🌱 has been activated!', labelEventCompleted: 'Taproot 🌱 has been activated!',
networks: ['mainnet'],
}, },
'840000': { '840000': {
labelEvent: 'Halving 🥳', labelEvent: 'Bitcoin\'s 4th Halving',
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1050000': {
labelEvent: 'Bitcoin\'s 5th Halving',
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1260000': {
labelEvent: 'Bitcoin\'s 6th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1470000': {
labelEvent: 'Bitcoin\'s 7th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1680000': {
labelEvent: 'Bitcoin\'s 8th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1890000': {
labelEvent: 'Bitcoin\'s 9th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2100000': {
labelEvent: 'Bitcoin\'s 10th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2310000': {
labelEvent: 'Bitcoin\'s 11th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2520000': {
labelEvent: 'Bitcoin\'s 12th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2730000': {
labelEvent: 'Bitcoin\'s 13th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2940000': {
labelEvent: 'Bitcoin\'s 14th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
networks: ['mainnet', 'testnet'],
},
'3150000': {
labelEvent: 'Bitcoin\'s 15th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
networks: ['mainnet', 'testnet'],
} }
}; };

View File

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

View File

@@ -17,7 +17,7 @@
<tbody *ngIf="blocks.value; else loadingTmpl"> <tbody *ngIf="blocks.value; else loadingTmpl">
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn"> <tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td> <td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since></td> <td><app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time></td>
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td> <td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
<td class="d-none d-md-block">{{ block.txs.length }}</td> <td class="d-none d-md-block">{{ block.txs.length }}</td>
</tr> </tr>

View File

@@ -1,11 +1,6 @@
.pagination-container { .pagination-container {
float: none; float: none;
margin-bottom: 200px;
@media(min-width: 400px){ @media(min-width: 400px){
float: right; float: right;
} }
} }
.container-xl {
padding-bottom: 110px;
}

View File

@@ -36,7 +36,7 @@
<h5 class="card-title">US Dollar - BTC/USD</h5> <h5 class="card-title">US Dollar - BTC/USD</h5>
<div class="chart-container"> <div class="chart-container">
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner"> <ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
<app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts> <app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
</ng-container> </ng-container>
</div> </div>
</div> </div>
@@ -84,7 +84,7 @@
</ng-template> </ng-template>
</td> </td>
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td> <td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -105,14 +105,6 @@
</ng-container> </ng-container>
</div> </div>
<app-language-selector></app-language-selector>
<div class="text-small text-center mt-3">
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
<a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a>
</div>
</div> </div>
<ng-template #loadingTmpl> <ng-template #loadingTmpl>
@@ -129,4 +121,4 @@
<ng-template #loading> <ng-template #loading>
<div class="skeleton-loader shorter"></div> <div class="skeleton-loader shorter"></div>
</ng-template> </ng-template>

View File

@@ -15,11 +15,9 @@
</span> </span>
<span class="grow"></span> <span class="grow"></span>
<div class="container-buttons"> <div class="container-buttons">
<button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right"> <div *ngIf="(latestBlock$ | async) as latestBlock">
<ng-container *ngTemplateOutlet="latestBlock.height - bisqTx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - bisqTx.blockHeight + 1}"></ng-container> <app-confirmations [chainTip]="latestBlock?.height" [height]="bisqTx.blockHeight" [hideUnconfirmed]="true" buttonClass="float-right"></app-confirmations>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template> </div>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
</div> </div>
</div> </div>
@@ -35,7 +33,7 @@
<td> <td>
&lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline"> <div class="lg-inline">
<i class="symbol">(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i> <i class="symbol">(<app-time kind="since" [time]="bisqTx.time / 1000" [fastRender]="true"></app-time>)</i>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -37,7 +37,7 @@
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span> {{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
</ng-template> </ng-template>
</td> </td>
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since></td> <td><app-time kind="since" [time]="tx.time / 1000" [fastRender]="true"></app-time></td>
<td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td> <td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -70,11 +70,7 @@
<div class="btn-container"> <div class="btn-container">
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock"> <span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
<button type="button" class="btn btn-sm btn-success mt-2"> <app-confirmations [chainTip]="latestBlock?.height" [height]="tx.blockHeight" [hideUnconfirmed]="true" buttonClass="mt-2"></app-confirmations>
<ng-container *ngTemplateOutlet="latestBlock.height - tx.blockHeight + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.blockHeight + 1}"></ng-container>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
&nbsp; &nbsp;
</span> </span>
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()"> <button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">

View File

@@ -254,3 +254,30 @@ export function selectPowerOfTen(val: number): { divider: number, unit: string }
return selectedPowerOfTen; return selectedPowerOfTen;
} }
const featureActivation = {
mainnet: {
rbf: 399701,
segwit: 477120,
taproot: 709632,
},
testnet: {
rbf: 720255,
segwit: 872730,
taproot: 2032291,
},
signet: {
rbf: 0,
segwit: 0,
taproot: 0,
},
};
export function isFeatureActive(network: string, height: number, feature: 'rbf' | 'segwit' | 'taproot'): boolean {
const activationHeight = featureActivation[network || 'mainnet']?.[feature];
if (activationHeight != null) {
return height >= activationHeight;
} else {
return false;
}
}

View File

@@ -1,7 +1,7 @@
<div class="container-xl about-page"> <div class="container-xl about-page">
<div class="intro"> <div class="intro">
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&trade;</span> <span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&reg;</span>
<img class="logo" src="/resources/mempool-logo-bigger.png" /> <img class="logo" src="/resources/mempool-logo-bigger.png" />
<div class="version"> <div class="version">
v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>] v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
@@ -13,7 +13,23 @@
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p> <p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
</div> </div>
<video src="/resources/mempool-promo.mp4" poster="/resources/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true"></video> <video #promoVideo (click)="unmutePromoVideo()" (touchstart)="unmutePromoVideo()" src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
<track label="English" kind="captions" srclang="en" src="/resources/promo-video/en.vtt" [attr.default]="showSubtitles('en') ? '' : null">
<track label="日本語" kind="captions" srclang="ja" src="/resources/promo-video/ja.vtt" [attr.default]="showSubtitles('ja') ? '' : null">
<track label="中文" kind="captions" srclang="zh" src="/resources/promo-video/zh.vtt" [attr.default]="showSubtitles('zh') ? '' : null">
<track label="Svenska" kind="captions" srclang="sv" src="/resources/promo-video/sv.vtt" [attr.default]="showSubtitles('sv') ? '' : null">
<track label="Čeština" kind="captions" srclang="cs" src="/resources/promo-video/cs.vtt" [attr.default]="showSubtitles('cs') ? '' : null">
<track label="Suomi" kind="captions" srclang="fi" src="/resources/promo-video/fi.vtt" [attr.default]="showSubtitles('fi') ? '' : null">
<track label="Français" kind="captions" srclang="fr" src="/resources/promo-video/fr.vtt" [attr.default]="showSubtitles('fr') ? '' : null">
<track label="Deutsch" kind="captions" srclang="de" src="/resources/promo-video/de.vtt" [attr.default]="showSubtitles('de') ? '' : null">
<track label="Italiano" kind="captions" srclang="it" src="/resources/promo-video/it.vtt" [attr.default]="showSubtitles('it') ? '' : null">
<track label="Lietuvių" kind="captions" srclang="lt" src="/resources/promo-video/lt.vtt" [attr.default]="showSubtitles('lt') ? '' : null">
<track label="Norsk" kind="captions" srclang="nb" src="/resources/promo-video/nb.vtt" [attr.default]="showSubtitles('nb') ? '' : null">
<track label="فارسی" kind="captions" srclang="fa" src="/resources/promo-video/fa.vtt" [attr.default]="showSubtitles('fa') ? '' : null">
<track label="Polski" kind="captions" srclang="pl" src="/resources/promo-video/pl.vtt" [attr.default]="showSubtitles('pl') ? '' : null">
<track label="Română" kind="captions" srclang="ro" src="/resources/promo-video/ro.vtt" [attr.default]="showSubtitles('ro') ? '' : null">
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<div class="enterprise-sponsor" id="enterprise-sponsors"> <div class="enterprise-sponsor" id="enterprise-sponsors">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3> <h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
@@ -91,22 +107,7 @@
<span>Blockstream</span> <span>Blockstream</span>
</a> </a>
<a href="https://unchained.com/" target="_blank" title="Unchained"> <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;"> <svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
<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> <span>Unchained</span>
</a> </a>
<a href="https://gemini.com/" target="_blank" title="Gemini"> <a href="https://gemini.com/" target="_blank" title="Gemini">
@@ -118,6 +119,18 @@
</svg> </svg>
<span>Gemini</span> <span>Gemini</span>
</a> </a>
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)" fill-rule="evenodd" clip-rule="evenodd" fill="#e21924">
<path d="M21.92 14.59a1.18 1.18 0 0 0-1.18-1.18h-1.82v2.36h1.82a1.18 1.18 0 0 0 1.18-1.18ZM21 17.07h-2v2.45h2a1.23 1.23 0 1 0 0-2.45Z"/>
<path d="M36.43 0 35 5.59l-8 2.64-2.43-3.61-4.74 2.05-4.74-2.05-2.43 3.61-8-2.64L3.21 0 0 7.86l7.89 5.86-5.56 4 5.56 1.12 2.69-.49v3.17l3.59 4.38.68 3.19 5 2.87 5-2.87.68-3.19 3.59-4.38v-3.17l2.7.49 5.56-1.12-5.56-4 7.89-5.86zM24.69 18.45a2.5 2.5 0 0 1-2.5 2.5h-1.11v1.56h-1.26V21h-.9v1.56h-1.27V21H15.3v-1.42h.64a.9.9 0 0 0 .9-.9V14.3a.901.901 0 0 0-.9-.91h-.64V12h2.35v-1.5h1.27V12h.9v-1.5h1.26V12h.68A2.269 2.269 0 0 1 24 14.31a2.25 2.25 0 0 1-.92 1.82 2.52 2.52 0 0 1 1.58 2.32z"/>
</g>
<defs>
<clipPath id="a"><path fill="#fff" d="M0 0h160v32H0z"/></clipPath>
</defs>
</svg>
<span>Bull Bitcoin</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus"> <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"> <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"/> <circle cx="250" cy="250" r="250" fill="#1F2033"/>
@@ -185,12 +198,12 @@
<span>Umbrel</span> <span>Umbrel</span>
</a> </a>
<a href="https://github.com/rootzoll/raspiblitz" target="_blank" title="RaspiBlitz"> <a href="https://github.com/rootzoll/raspiblitz" target="_blank" title="RaspiBlitz">
<img class="image" src="/resources/profile/raspiblitz.jpg" /> <img class="image" src="/resources/profile/raspiblitz.svg" />
<span>RaspiBlitz</span> <span>RaspiBlitz</span>
</a> </a>
<a href="https://github.com/mynodebtc/mynode" target="_blank" title="MyNode"> <a href="https://github.com/mynodebtc/mynode" target="_blank" title="myNode">
<img class="image" src="/resources/profile/mynodebtc.jpg" /> <img class="image" src="/resources/profile/mynodebtc.png" />
<span>MyNode</span> <span>myNode</span>
</a> </a>
<a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo"> <a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo">
<img class="image" src="/resources/profile/ronindojo.png" /> <img class="image" src="/resources/profile/ronindojo.png" />
@@ -204,12 +217,12 @@
<img class="image" src="/resources/profile/nix-bitcoin.png" /> <img class="image" src="/resources/profile/nix-bitcoin.png" />
<span>NixOS</span> <span>NixOS</span>
</a> </a>
<a href="https://github.com/Start9Labs/embassy-os" target="_blank" title="EmbassyOS"> <a href="https://github.com/Start9Labs/start-os" target="_blank" title="StartOS">
<img class="image" src="/resources/profile/start9.png" /> <img class="image" src="/resources/profile/start9.png" />
<span>EmbassyOS</span> <span>StartOS</span>
</a> </a>
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server"> <a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
<img class="image" src="/resources/profile/btcpayserver.svg" /> <img class="image not-rounded" src="/resources/profile/btcpayserver.svg" />
<span>BTCPay</span> <span>BTCPay</span>
</a> </a>
<a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq"> <a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
@@ -237,7 +250,7 @@
<span>Sparrow</span> <span>Sparrow</span>
</a> </a>
<a href="https://github.com/ACINQ/phoenix" target="_blank" title="Phoenix Wallet by ACINQ"> <a href="https://github.com/ACINQ/phoenix" target="_blank" title="Phoenix Wallet by ACINQ">
<img class="image" src="/resources/profile/phoenix.jpg" /> <img class="image not-rounded" src="/resources/profile/phoenix.svg" />
<span>Phoenix</span> <span>Phoenix</span>
</a> </a>
<a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits"> <a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits">
@@ -268,6 +281,26 @@
<img class="image" src="/resources/profile/nunchuk.svg" /> <img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span> <span>Nunchuk</span>
</a> </a>
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
<img class="image" src="/resources/profile/bitcoin-s.svg" />
<span>bitcoin-s</span>
</a>
<a href="https://github.com/EdgeApp" target="_blank" title="Edge">
<img class="image not-rounded" src="/resources/profile/edge.svg" />
<span>Edge</span>
</a>
<a href="https://github.com/GaloyMoney" target="_blank" title="Galoy">
<img class="image" src="/resources/profile/galoy.svg" />
<span>Galoy</span>
</a>
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
<img class="image" src="/resources/profile/boltz.svg" />
<span>Boltz</span>
</a>
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
<span>Mutiny</span>
</a>
</div> </div>
</div> </div>
@@ -363,7 +396,7 @@
Trademark Notice<br> Trademark Notice<br>
</div> </div>
<p> <p>
The Mempool Open Source Project&trade;, mempool.space&trade;, the mempool logo&trade;, the mempool.space logos&trade;, the mempool square logo&trade;, and the mempool blocks logo&trade; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. The Mempool Open Source Project&trade;, mempool.space&trade;, the mempool logo&reg;, the mempool.space logos&trade;, the mempool square logo&reg;, and the mempool blocks logo&trade; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p> </p>
<p> <p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;. While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.
@@ -372,27 +405,14 @@
<div class="footer-links"> <div class="footer-links">
<a href="/3rdpartylicenses.txt">Third-party Licenses</a> <a href="/3rdpartylicenses.txt">Third-party Licenses</a>
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
<div class="social-icons">
<a target="_blank" href="https://github.com/mempool/mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
</a>
<a target="_blank" href="https://twitter.com/mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" class="svg-inline--fa fa-twitter fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
</a>
<a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg>
</a>
</div>
</div>
<div class="footer-version" *ngIf="officialMempoolSpace">
{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]
</div> </div>
<br>
</div> </div>
<ng-template #loadingSponsors> <ng-template #loadingSponsors>
<br> <br>
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</ng-template> </ng-template>

View File

@@ -11,6 +11,12 @@
line-height: 32px; line-height: 32px;
} }
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro { .intro {
margin: 25px auto 30px; margin: 25px auto 30px;
width: 250px; width: 250px;
@@ -36,9 +42,11 @@
video { video {
width: 640px; width: 640px;
height: 360px;
max-width: 90%; max-width: 90%;
margin-top: 0; margin-top: 0;
@media (min-width: 768px) {
height: 360px;
}
} }
.social-icons { .social-icons {
@@ -51,9 +59,13 @@
.enterprise-sponsor, .enterprise-sponsor,
.community-integrations-sponsor, .community-integrations-sponsor,
.maintainers { .maintainers {
margin-top: 68px; margin-top: 30px;
margin-bottom: 68px; margin-bottom: 68px;
scroll-margin: 30px; scroll-margin: 30px;
@media (min-width: 768px) {
margin-top: 68px;
}
} }
.maintainers { .maintainers {
@@ -199,6 +211,16 @@
a { a {
margin: 45px 10px; margin: 45px 10px;
} }
.bitcointv svg {
width: 36px;
height: auto;
vertical-align: bottom;
margin-bottom: 2px;
margin-left: 5px;
}
.bitcointv svg:hover {
opacity: 0.75;
}
} }
} }
@@ -212,6 +234,11 @@
} }
.community-integrations-sponsor { .community-integrations-sponsor {
max-width: 965px; max-width: 1110px;
margin: auto; margin: auto;
} }
.community-integrations-sponsor img.image {
width: 64px;
height: 64px;
}

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
@@ -17,6 +17,7 @@ import { DOCUMENT } from '@angular/common';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AboutComponent implements OnInit { export class AboutComponent implements OnInit {
@ViewChild('promoVideo') promoVideo: ElementRef;
backendInfo$: Observable<IBackendInfo>; backendInfo$: Observable<IBackendInfo>;
sponsors$: Observable<any>; sponsors$: Observable<any>;
translators$: Observable<ITranslators>; translators$: Observable<ITranslators>;
@@ -68,7 +69,7 @@ export class AboutComponent implements OnInit {
tap(() => this.goToAnchor()) tap(() => this.goToAnchor())
); );
} }
ngAfterViewInit() { ngAfterViewInit() {
this.goToAnchor(); this.goToAnchor();
} }
@@ -90,4 +91,12 @@ export class AboutComponent implements OnInit {
this.showNavigateToSponsor = true; this.showNavigateToSponsor = true;
} }
} }
showSubtitles(language): boolean {
return ( this.locale.startsWith( language ) && !this.locale.startsWith('en') );
}
unmutePromoVideo(): void {
this.promoVideo.nativeElement.muted = false;
}
} }

View File

@@ -1,15 +1,15 @@
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin"> <ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
<span class="fiat" *ngIf="blockConversion; else noblockconversion"> <span class="fiat" *ngIf="blockConversion; else noblockconversion">
{{ addPlus && satoshis >= 0 ? '+' : '' }} {{ addPlus && satoshis >= 0 ? '+' : '' }}{{
{{
( (
(blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ?? (blockConversion.price[currency] > -1 ? blockConversion.price[currency] : null) ??
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0 (blockConversion.price['USD'] > -1 ? blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency] : null) ?? 0
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency ) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
}} }}
</span> </span>
<ng-template #noblockconversion> <ng-template #noblockconversion>
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}</span> <span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions[currency] > -1 ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}
</span>
</ng-template> </ng-template>
</ng-container> </ng-container>

View File

@@ -82,6 +82,10 @@
<br /> <br />
<router-outlet></router-outlet> <main>
<router-outlet></router-outlet>
</main>
<app-global-footer *ngIf="footerVisible"></app-global-footer>
<br> <br>

View File

@@ -17,6 +17,12 @@ li.nav-item {
padding-right: 10px; padding-right: 10px;
} }
@media (max-width: 992px) {
footer > .container-fluid {
padding-bottom: 35px;
}
}
@media (min-width: 992px) { @media (min-width: 992px) {
.navbar { .navbar {
padding: 0rem 2rem; padding: 0rem 2rem;

View File

@@ -17,6 +17,7 @@ export class BisqMasterPageComponent implements OnInit {
isMobile = window.innerWidth <= 767.98; isMobile = window.innerWidth <= 767.98;
urlLanguage: string; urlLanguage: string;
networkPaths: { [network: string]: string }; networkPaths: { [network: string]: string };
footerVisible = true;
constructor( constructor(
private stateService: StateService, private stateService: StateService,
@@ -31,6 +32,11 @@ export class BisqMasterPageComponent implements OnInit {
this.urlLanguage = this.languageService.getLanguageForUrl(); this.urlLanguage = this.languageService.getLanguageForUrl();
this.navigationService.subnetPaths.subscribe((paths) => { this.navigationService.subnetPaths.subscribe((paths) => {
this.networkPaths = paths; this.networkPaths = paths;
if (paths.mainnet.indexOf('docs') > -1) {
this.footerVisible = false;
} else {
this.footerVisible = true;
}
}); });
} }

View File

@@ -4,6 +4,9 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -18,17 +21,19 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
min-height: 500px; height: calc(100vh - 250px);
height: calc(100% - 150px); @media (min-width: 992px) {
@media (max-width: 992px) { height: calc(100vh - 150px);
padding-bottom: 100px; }
};
} }
.chart { .chart {
width: 100%; display: flex;
flex: 1;
height: 100%; height: 100%;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px; padding-right: 10px;

View File

@@ -4,6 +4,9 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -18,18 +21,20 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
min-height: 500px; height: calc(100vh - 250px);
height: calc(100% - 150px); @media (min-width: 992px) {
@media (max-width: 992px) { height: calc(100vh - 150px);
padding-bottom: 100px; }
};
} }
.chart { .chart {
display: flex;
flex: 1;
width: 100%; width: 100%;
height: 100%;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px; padding-right: 10px;
@media (max-width: 992px) { @media (max-width: 992px) {
@@ -54,31 +59,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@@ -3,7 +3,7 @@
<div class="full-container"> <div class="full-container">
<div class="card-header mb-0 mb-md-4"> <div class="card-header mb-0 mb-md-4">
<div class="d-flex d-md-block align-items-baseline"> <div class="d-flex d-md-block align-items-baseline">
<span i18n="mining.block-prediction-accuracy">Block Prediction Accuracy</span> <span i18n="mining.blocks-health">Block Health</span>
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()"> <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button> </button>
@@ -12,34 +12,34 @@
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}"> <div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 24h <input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 24h
</label> </label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3D <input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3D
</label> </label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1W <input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1W
</label> </label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1M <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1M
</label> </label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3M <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3M
</label> </label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 6M
</label> </label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 1Y
</label> </label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 2Y
</label> </label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> 3Y
</label> </label>
<label class="btn btn-primary btn-sm" *ngIf="stats.blockCount > 157680" [class.active]="radioGroupForm.get('dateSpan').value === 'all'"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount > 157680" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> ALL <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-health' | relativeUrl]" formControlName="dateSpan"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@@ -4,6 +4,9 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -18,17 +21,19 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
min-height: 500px; height: calc(100vh - 250px);
height: calc(100% - 150px); @media (min-width: 992px) {
@media (max-width: 992px) { height: calc(100vh - 150px);
padding-bottom: 100px; }
};
} }
.chart { .chart {
width: 100%; display: flex;
flex: 1;
height: 100%; height: 100%;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px; padding-right: 10px;
@@ -54,31 +59,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@@ -13,9 +13,9 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
@Component({ @Component({
selector: 'app-block-prediction-graph', selector: 'app-block-health-graph',
templateUrl: './block-prediction-graph.component.html', templateUrl: './block-health-graph.component.html',
styleUrls: ['./block-prediction-graph.component.scss'], styleUrls: ['./block-health-graph.component.scss'],
styles: [` styles: [`
.loadingGraphs { .loadingGraphs {
position: absolute; position: absolute;
@@ -26,7 +26,7 @@ import { StateService } from '../../services/state.service';
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class BlockPredictionGraphComponent implements OnInit { export class BlockHealthGraphComponent implements OnInit {
@Input() right: number | string = 45; @Input() right: number | string = 45;
@Input() left: number | string = 75; @Input() left: number | string = 75;
@@ -60,7 +60,7 @@ export class BlockPredictionGraphComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Prediction Accuracy`); this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health`);
this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h'); this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -80,7 +80,7 @@ export class BlockPredictionGraphComponent implements OnInit {
this.storageService.setValue('miningWindowPreference', timespan); this.storageService.setValue('miningWindowPreference', timespan);
this.timespan = timespan; this.timespan = timespan;
this.isLoading = true; this.isLoading = true;
return this.apiService.getHistoricalBlockPrediction$(timespan) return this.apiService.getHistoricalBlocksHealth$(timespan)
.pipe( .pipe(
tap((response) => { tap((response) => {
this.prepareChartOptions(response.body); this.prepareChartOptions(response.body);
@@ -163,7 +163,7 @@ export class BlockPredictionGraphComponent implements OnInit {
hideOverlap: true, hideOverlap: true,
padding: [0, 5], padding: [0, 5],
}, },
data: data.map(prediction => prediction[0]) data: data.map(health => health[0])
}, },
yAxis: data.length === 0 ? undefined : [ yAxis: data.length === 0 ? undefined : [
{ {
@@ -186,12 +186,12 @@ export class BlockPredictionGraphComponent implements OnInit {
series: data.length === 0 ? undefined : [ series: data.length === 0 ? undefined : [
{ {
zlevel: 0, zlevel: 0,
name: $localize`Match rate`, name: $localize`Health`,
data: data.map(prediction => ({ data: data.map(health => ({
value: prediction[2], value: health[2],
block: prediction[1], block: health[1],
itemStyle: { itemStyle: {
color: this.getPredictionColor(prediction[2]) color: this.getHealthColor(health[2])
} }
})), })),
type: 'bar', type: 'bar',
@@ -257,7 +257,7 @@ export class BlockPredictionGraphComponent implements OnInit {
return 'rgb(' + gradient.red + ',' + gradient.green + ',' + gradient.blue + ')'; return 'rgb(' + gradient.red + ',' + gradient.green + ',' + gradient.blue + ')';
} }
getPredictionColor(matchRate) { getHealthColor(matchRate) {
return this.colorGradient( return this.colorGradient(
Math.pow((100 - matchRate) / 100, 0.5), Math.pow((100 - matchRate) / 100, 0.5),
{red: 67, green: 171, blue: 71}, {red: 67, green: 171, blue: 71},
@@ -294,7 +294,7 @@ export class BlockPredictionGraphComponent implements OnInit {
download(this.chartInstance.getDataURL({ download(this.chartInstance.getDataURL({
pixelRatio: 2, pixelRatio: 2,
excludeComponents: ['dataZoom'], excludeComponents: ['dataZoom'],
}), `block-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`); }), `block-health-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore // @ts-ignore
this.chartOptions.grid.bottom = prevBottom; this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none'; this.chartOptions.backgroundColor = 'none';

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