Compare commits

..

203 Commits

Author SHA1 Message Date
hunicus
93dab41e9e Show tool list when no other networks available 2023-08-01 18:35:51 +09:00
hunicus
8fe78fa12b Improve conditions for showing network links 2023-08-01 18:08:49 +09:00
hunicus
f166cb7974 Remove commented/inactive social links 2023-08-01 17:54:09 +09:00
hunicus
d3532eb734 Improve show conditions for explore links 2023-08-01 17:54:09 +09:00
hunicus
4cb379bb0f Change more networks to networks 2023-08-01 17:54:09 +09:00
hunicus
316028fe66 Add space below tagline 2023-08-01 17:54:09 +09:00
wiz
c93f52f3a8 Merge branch 'master' into hunicus/30-footer 2023-08-01 16:54:48 +09:00
softsimon
22e57ae95c Merge pull request #4079 from mempool/simon/mempool-break-poll-limit
Base mempool break limit of current poll rate
2023-08-01 16:00:42 +09:00
hunicus
a1af41804a Implement wiz footer suggestions 2023-08-01 16:00:16 +09:00
softsimon
b0080a5859 Base mempool break limit of current poll rate 2023-08-01 15:55:03 +09:00
wiz
5c36692799 Merge pull request #3973 from mempool/ops/move-electrs-scripts-to-electrs-repo
ops: Move electrs scripts to mempool/electrs repo
2023-08-01 15:38:46 +09:00
wiz
97877053bf Merge pull request #4068 from mempool/mononaut/electrs-blocks
Get blocks from electrs again
2023-08-01 15:33:13 +09:00
softsimon
f9a44a5fbb Merge pull request #4074 from mempool/fix_rust_gbt_typo
Fix rust gbt docker  typo
2023-08-01 14:14:56 +09:00
softsimon
6b5bcaa279 Merge pull request #4077 from bguillaumat/patch-1
Add bguillaumat.txt to contributors
2023-08-01 14:07:18 +09:00
wiz
8c396978a8 Merge branch 'master' into mononaut/electrs-blocks 2023-08-01 13:54:05 +09:00
wiz
a51d2a6aec Merge pull request #4071 from mempool/mononaut/fast-indexing
Fast indexing
2023-08-01 13:53:56 +09:00
Mononaut
a863c17408 Fix difficulty indexing db queries to return bits 2023-08-01 13:28:56 +09:00
Mononaut
0924bb6ac0 Use bits to detect difficulty adjustments, not difficulty 2023-08-01 13:28:56 +09:00
Mononaut
910e67ff36 Get blocks from electrs again 2023-08-01 13:28:56 +09:00
Bastien Guillaumat
aa17f8203c Add bguillaumat.txt to contributors 2023-08-01 03:13:58 +02:00
Felipe Knorr Kuhn
b7b6548cce Fix RUST GBT Docker override 2023-07-31 14:11:31 -07:00
Felipe Knorr Kuhn
17f1cb8648 Fix config unit test that was returning early 2023-07-31 14:10:49 -07:00
softsimon
d5dca95fbe Merge pull request #4072 from mempool/simon/electrum-scripthash-fix
Fix scripthash lookup for Electrum*
2023-07-31 21:10:59 +09:00
softsimon
bed7c1b283 Fix scripthash lookup for Electrum* 2023-07-31 18:29:40 +09:00
Mononaut
0d25ef0b5b Get block txs from esplora, index CPFP together with summaries 2023-07-31 18:13:16 +09:00
Mononaut
6b7d8d95f7 reduce mempool poll rate while indexing 2023-07-31 18:13:16 +09:00
Mononaut
bafc0bd9cf fix indexing log prints 2023-07-31 18:13:11 +09:00
softsimon
7a87f74b22 Merge pull request #4070 from mempool/mononaut/redis-fixes
Misc Redis fixes
2023-07-31 17:28:54 +09:00
Mononaut
49db63d888 Faster Redis tx deletion, fix debug log level 2023-07-31 16:38:18 +09:00
Mononaut
363fc1b00b Get blocks from electrs again 2023-07-31 15:39:02 +09:00
wiz
36a26fc2ce Merge pull request #4063 from mempool/mononaut/simple-redis
Simple Redis
2023-07-31 15:34:08 +09:00
softsimon
73b71c4914 Fixing docker config and tests 2023-07-31 14:28:56 +09:00
Mononaut
dcfab218fb Improve Redis logging 2023-07-31 12:21:28 +09:00
Mononaut
c79a597c96 switch from redis-json to simple key-value redis entries 2023-07-31 12:16:37 +09:00
Mononaut
a393f42b5e strip non-essential data from redis cache txs 2023-07-31 12:16:36 +09:00
Mononaut
6ac58f2da7 store redis mempool in sharded json object 2023-07-31 12:16:36 +09:00
Mononaut
a9f8bbbcce Add network and schema versioning to redis cache 2023-07-31 12:16:34 +09:00
Mononaut
d65bddd30b Add transactions to Redis cache in manageable batches 2023-07-31 12:16:34 +09:00
Mononaut
b6cb539470 Fix redis feature merge conflicts 2023-07-31 12:16:34 +09:00
Mononaut
aea2b1ec6b Add RBF data to Redis cache 2023-07-31 12:16:33 +09:00
Mononaut
5138f9a254 Implement Redis cache for block and mempool data 2023-07-31 12:16:33 +09:00
wiz
8cfa4ef1a1 Merge pull request #4066 from mempool/wiz/update-preview-image
Remove text from mempool.space preview image
2023-07-31 11:24:17 +09:00
wiz
16401044f6 Remove text from mempool.space preview image 2023-07-31 11:16:43 +09:00
wiz
c88b7ddc77 ops: Use TK7 for unfurler 2023-07-31 11:12:54 +09:00
softsimon
8b012a96f3 Merge pull request #4034 from mempool/mononaut/mobile-difficulty-tooltips
Fix difficulty tooltip position
2023-07-30 14:26:36 +09:00
softsimon
8b6bb54efb Merge pull request #4062 from fiatjaf/patch-1
Create fiatjaf.txt
2023-07-29 23:52:32 +09:00
fiatjaf_
2670589293 Create fiatjaf.txt 2023-07-29 09:27:12 -03:00
softsimon
91eef1c4d9 Merge pull request #4061 from rishkwal/master
rishkwal contributor license agreement added
2023-07-29 21:14:02 +09:00
Rishabh
562a5f6878 rishkwal contributor license agreement added 2023-07-29 15:54:34 +05:30
softsimon
5c20fd71e1 Merge pull request #3905 from mempool/hunicus/msop-r
Update mosp strings from tm to r
2023-07-29 18:34:31 +09:00
softsimon
adf093eca5 Merge pull request #4026 from mempool/nymkappa/charts-landscape-mobile
[graphs] fix min height in mobile landscape
2023-07-29 18:11:33 +09:00
softsimon
d14d286f24 Merge pull request #4057 from dni/patch-1
[BUG]: Update frontend entrypoint.sh
2023-07-29 17:50:58 +09:00
softsimon
ecd80aad6a Merge pull request #4056 from mempool/mononaut/compressed-p2pk
Add support for compressed p2pk addresses
2023-07-29 17:48:26 +09:00
softsimon
1b248c24f1 Merge pull request #4050 from mempool/mononaut/retry-block-txs
Handle failures while fetching block transactions
2023-07-29 17:12:34 +09:00
softsimon
f35f630695 Merge pull request #4060 from Czino/add-contributor-license-agreement
Add contributor license agreement
2023-07-29 17:09:30 +09:00
softsimon
8172ec9245 Merge pull request #4053 from mempool/nymkappa/pool-logo
[mining] use .slug to load pool logo
2023-07-29 17:07:06 +09:00
softsimon
14c86b84b8 Merge pull request #4059 from mempool/mononaut/connection-state
fix websocket connection state observable
2023-07-29 17:05:17 +09:00
Czino
1f8f40011a Update date 2023-07-29 09:57:40 +02:00
Czino
2719be9075 Add contributor license agreement 2023-07-29 09:53:47 +02:00
Mononaut
354c119e99 fix websocket connection state observable 2023-07-29 15:22:15 +09:00
dni ⚡
cc27c0159e [BUG]: Update frontend entrypoint.sh
Typo of variable LIQUID_ENABLED
2023-07-28 23:04:44 +02:00
wiz
b1bdb52851 ops: Fix a classic typo in mempool clear protection log print 2023-07-28 23:40:06 +09:00
softsimon
d52e2cd585 Merge pull request #4054 from mempool/mononaut/mined-address-txs
Show new mined transactions on the address page
2023-07-28 20:43:22 +09:00
Mononaut
2c613195cc Add support for compressed p2pk addresses 2023-07-28 19:17:52 +09:00
wiz
1ca99e9967 Merge branch 'master' into hunicus/msop-r 2023-07-28 17:29:25 +09:00
softsimon
f9ddc3cc5f Merge pull request #4049 from mempool/mononaut/p2pk-websocket-subscription
Support p2pk track-address websocket subscriptions
2023-07-28 17:03:24 +09:00
Mononaut
63ccecf410 remove unused calcScriptHash function 2023-07-28 16:14:28 +09:00
Mononaut
5b2470955d track p2pk addresses by scriptpubkey not scripthash 2023-07-28 16:09:39 +09:00
Mononaut
74b87b6006 Support p2pk track-address websocket subscriptions 2023-07-28 16:09:39 +09:00
Mononaut
9b65fbd98c Show new mined transactions on the address page 2023-07-28 15:56:55 +09:00
nymkappa
3f3f0db2f2 [mining] use .slug to load pool logo 2023-07-28 13:45:04 +09:00
nymkappa
c993ee51cc Merge branch 'master' into nymkappa/charts-landscape-mobile 2023-07-28 09:37:45 +09:00
softsimon
395f47516a Merge pull request #4048 from mempool/mononaut/fix-key-event-leak
Fix key navigation subscription leak
2023-07-27 17:20:30 +09:00
Mononaut
589adb95c3 remove stray debugging log 2023-07-27 14:49:21 +09:00
Mononaut
1fd5b975f1 Handle failures while fetching block transactions 2023-07-27 11:45:16 +09:00
softsimon
7edd40246c Merge branch 'master' into nymkappa/charts-landscape-mobile 2023-07-25 21:37:35 +09:00
Mononaut
e15c0c6c7a Fix key navigation subscription leak 2023-07-25 21:18:19 +09:00
softsimon
a13c424869 Merge pull request #4046 from mempool/mononaut/audit-exclude-conflicts
Exclude all conflicting transactions from audit score
2023-07-25 20:22:37 +09:00
softsimon
8ee9f52634 Merge pull request #4028 from mempool/nymkappa/search-autofocus
[search bar] only autofocus when in `/`, `/mining` and `/lightning`
2023-07-25 18:19:17 +09:00
softsimon
b58abe4779 Merge pull request #4045 from mempool/mononaut/fast-blocks
Faster blocks
2023-07-25 16:07:52 +09:00
nymkappa
6d5be78dd0 [search bar] use afterviewinit instead of afterviewchecked 2023-07-25 15:03:39 +09:00
softsimon
07b0f24cf1 Update frontend/src/app/shared/pipes/bytes-pipe/utils.ts 2023-07-25 14:26:43 +09:00
Mononaut
d7b874ac49 Exclude all conflicting transactions from audit score 2023-07-25 14:17:02 +09:00
nymkappa
0a0978f7d7 Merge branch 'master' into nymkappa/search-autofocus 2023-07-25 13:31:15 +09:00
Mononaut
25925751eb refactor $getTransactionsExtended to optimise API requests 2023-07-25 12:09:13 +09:00
Mononaut
0ebfd6f017 Fetch block txs from mempool/electrs in bulk 2023-07-25 10:27:43 +09:00
wiz
81d1c0a4d5 Merge pull request #4043 from mempool/mononaut/mempool-sync-status
Mempool inSync status
2023-07-24 19:39:17 +09:00
Mononaut
36fe5627c7 fix mempool sync skeleton loaders on Core backend 2023-07-24 17:49:34 +09:00
nymkappa
9e43dadad8 Merge branch 'master' into nymkappa/search-autofocus 2023-07-24 17:28:47 +09:00
Mononaut
2d463326e0 fix gbt mempool size mismatch bug 2023-07-24 17:22:38 +09:00
Mononaut
a6edfcc272 show mempool skeleton while not inSync 2023-07-24 16:22:35 +09:00
Mononaut
de4265a6d1 More conservative mempool inSync status 2023-07-24 16:22:22 +09:00
softsimon
dc43a81899 Merge pull request #4035 from mempool/mononaut/lightning-balance-bars
Lightning channel balance progress bars
2023-07-24 16:22:20 +09:00
Mononaut
e59c961f25 Add electrs sync progress updates 2023-07-24 14:59:51 +09:00
Mononaut
db715a1dba Switch to batch mempool/txs/:txid endpoint 2023-07-24 14:44:43 +09:00
Mononaut
202d4122b4 load mempool txs in bulk from esplora 2023-07-24 14:44:42 +09:00
nymkappa
f62f2341f4 Merge branch 'master' into nymkappa/search-autofocus 2023-07-24 13:39:17 +09:00
softsimon
e2fdacfddd Merge pull request #4041 from mempool/simon/sanitize-lightning-channel-id
Sanitize channel id search
2023-07-24 13:26:20 +09:00
softsimon
c84a444f79 Merge pull request #4039 from mempool/mononaut/fix-sigops
Fix missing sigops
2023-07-24 13:26:13 +09:00
softsimon
ee2d8f8c5a Sanitize channel id search 2023-07-24 13:21:06 +09:00
nymkappa
7db391d762 [search bar] add missing autofocus on lightning dashboard 2023-07-24 11:51:15 +09:00
nymkappa
da4a20cb85 [search bar] dont auto focus if touch screen 2023-07-24 11:35:46 +09:00
Mononaut
44f2217a68 Fix typo which skips sigop calculation 2023-07-24 10:49:29 +09:00
nymkappa
6ce3c1d75d [search bar] auto focus only in dashboards 2023-07-24 10:18:00 +09:00
wiz
caa8cfbc0e Another hotfix for CLN crash 2023-07-23 22:35:32 +09:00
wiz
02f361af73 Hotfix for CLN crash 2023-07-23 22:21:53 +09:00
Mononaut
a1e05c0c37 Lightning channel balance progress bars 2023-07-23 18:00:24 +09:00
Mononaut
05affa5ad4 Fix difficulty tooltip position 2023-07-23 16:19:48 +09:00
softsimon
5e91af168b Merge pull request #4027 from mempool/mononaut/p2pk
Support P2PK address types
2023-07-23 15:06:04 +09:00
softsimon
ae183210e0 Updating pubkey width on mobile and desktop 2023-07-23 14:43:43 +09:00
Mononaut
56127dce6a Add P2PK support to search bar 2023-07-23 14:05:04 +09:00
Mononaut
0376467e6c highlight matching P2PK inputs 2023-07-23 14:01:31 +09:00
Mononaut
48b55eed46 improve script hex parsing validation 2023-07-23 14:01:31 +09:00
Mononaut
0ce043cca9 Fix esplora error messages 2023-07-23 14:01:31 +09:00
Mononaut
65dbafd2ec Support P2PK address types 2023-07-23 14:01:31 +09:00
softsimon
3f36a30d1d Merge pull request #4031 from knorrium/es2022_fixes
Es2022 fixes
2023-07-23 11:56:04 +09:00
softsimon
b021746e9e Merge pull request #4030 from mempool/fix/CI-Rust
Use more reliable Github Action for Rust toolchain install
2023-07-23 11:50:00 +09:00
junderw
975ec772fa Use more reliable Github Action for Rust toolchain install. 2023-07-22 19:41:36 -07:00
Felipe Knorr Kuhn
442a4ff6e0 Fix tsconfig settigns for ES2022 2023-07-23 11:06:21 +09:00
Felipe Knorr Kuhn
cea218b81a Reset the supported browsers list 2023-07-23 11:05:49 +09:00
nymkappa
7970df27ad [graphs] fix min height in mobile landscape 2023-07-22 17:42:02 +09:00
softsimon
a24d2ce547 Merge pull request #4021 from mempool/mononaut/blockchain-scroll
apply blockchain scroll offset as soon as element is ready
2023-07-22 14:32:42 +09:00
softsimon
95707de8ec Merge pull request #4013 from mempool/simon/css-fix-fa-icons
Fix some icon css color changes
2023-07-22 13:53:55 +09:00
softsimon
eb37066d5d Merge pull request #4024 from devinbileck/patch-1
Accept CLA
2023-07-22 13:53:45 +09:00
Devin Bileck
f0983844c1 Accept CLA 2023-07-21 15:13:10 -07:00
softsimon
a0bd4e0f63 Merge branch 'master' into mononaut/blockchain-scroll 2023-07-21 21:12:11 +09:00
softsimon
141ab8076f Merge pull request #4014 from mempool/mononaut/fix-blocks-list
Fix blocks list observable
2023-07-21 21:12:03 +09:00
softsimon
267f3d4877 Merge pull request #4020 from knorrium/fix_rate_limiting
Fix rate limiting when syncing assets on CI
2023-07-21 21:11:41 +09:00
softsimon
460a41644d Merge pull request #4015 from pedromvpg/patch-1
sign contributor agreement
2023-07-21 21:04:31 +09:00
Felipe Knorr Kuhn
ca69d19bf7 Use the GITHUB_SECRET to authenticate with the API
Fix the environment variable

Add extra logging when using the authentication

Use the GITHUB_TOKEN on the frontend build step
2023-07-21 18:14:32 +09:00
Mononaut
d91fa5c6ef null => of([]) 2023-07-21 18:10:13 +09:00
wiz
3610aa2e20 Merge pull request #3999 from mempool/mononaut/fix-liquid-fees
Fix fee handling on Liquid
2023-07-21 17:49:48 +09:00
wiz
7a6da07a61 Merge branch 'master' into mononaut/fix-liquid-fees 2023-07-21 17:38:47 +09:00
Mononaut
0f77fb88bf handle missing block.extras on liquid 2023-07-21 17:18:45 +09:00
Mononaut
1bd19e1d8d apply blockchain scroll offset when element is ready 2023-07-21 17:10:58 +09:00
Felipe Knorr Kuhn
61eeb82694 Expose the GITHUB_SECRET to the frontend build step 2023-07-21 17:09:57 +09:00
softsimon
135adfecbd Merge pull request #3934 from mempool/junderw/fix-armv7-docker
Fix backend docker build for armv7
2023-07-21 10:19:20 +09:00
softsimon
20b2017908 Merge pull request #4016 from knorrium/tweak_dependabot
Tweak dependabot settings
2023-07-21 10:18:58 +09:00
Felipe Knorr Kuhn
b1345038bd Tweak dependabot settings 2023-07-20 18:09:36 -07:00
Felipe Knorr Kuhn
7ba627e243 Merge branch 'master' into junderw/fix-armv7-docker 2023-07-20 17:31:17 -07:00
Pedro
6b453ef018 sign contributor agreement 2023-07-20 17:24:13 +01:00
Mononaut
943dc6f5e6 Fix blocks list observable 2023-07-20 17:30:26 +09:00
softsimon
4192869593 Fix some icon css color changes 2023-07-20 16:02:04 +09:00
softsimon
e066bb1e9d Merge pull request #4002 from mempool/nymkappa/mining-pool-summary
[mining] add missing empty td at the bottom of pool ranking
2023-07-20 15:10:32 +09:00
softsimon
6cdc97848f Merge pull request #3773 from mempool/nymkappa/search-bar-align
[search bar] fix alignment issue
2023-07-20 14:59:54 +09:00
nymkappa
ade7908229 [mining] add missing empty col at the bottom of pool ranking 2023-07-20 10:36:26 +09:00
nymkappa
9a2ab7fe21 [search bar] chrome - fix flex-auto 2023-07-20 09:57:04 +09:00
Mononaut
87e39b8389 Fix liquid blockchain bar 2023-07-19 16:24:05 +09:00
softsimon
bc508b6621 Merge pull request #3949 from knorrium/node_v20_to_matrix
Add node v20 to the test matrix
2023-07-19 15:56:31 +09:00
Mononaut
709783280a Fix liquid fees & remove minimum fee rate 2023-07-19 15:42:02 +09:00
softsimon
f9aa1b5b35 Merge pull request #3194 from mempool/hunicus/manual-deployment-enterprise
Specify manual deployment support for enterprise sponsors
2023-07-19 14:25:02 +09:00
softsimon
2fffd8b43c Merge branch 'master' into hunicus/manual-deployment-enterprise 2023-07-19 14:19:03 +09:00
softsimon
741571a93a Merge pull request #3607 from mempool/nymkappa/clip-label-overflow
Clip overflowing labels in pool component on mobile
2023-07-19 14:05:31 +09:00
softsimon
07a424e6f1 Merge pull request #3997 from mempool/mononaut/liquid-latest-blocks
restore latest blocks on liquid
2023-07-19 14:03:58 +09:00
Mononaut
943d05f680 restore latest blocks on liquid 2023-07-19 12:09:46 +09:00
softsimon
57aa2c69ac Merge branch 'master' into nymkappa/clip-label-overflow 2023-07-19 11:31:01 +09:00
softsimon
61e28a33e0 Merge pull request #3996 from mempool/simon/zero-zat-minimum-fee-fix
Fix for 0 sat minimum fee
2023-07-19 11:15:53 +09:00
softsimon
4055128d7f Fix for 0 sat minimum fee 2023-07-19 11:06:28 +09:00
Felipe Knorr Kuhn
7c29e51bbb Merge branch 'master' into junderw/fix-armv7-docker 2023-07-18 14:30:20 -07:00
softsimon
fabd420586 Merge branch 'master' into node_v20_to_matrix 2023-07-18 18:56:00 +09:00
softsimon
a12316f4dc Updating default mining pool icon 2023-07-18 18:16:21 +09:00
softsimon
548611f13a Merge branch 'master' into nymkappa/search-bar-align 2023-07-18 18:02:44 +09:00
softsimon
3397edecb4 Merge pull request #3659 from nothing0012/zz/add-cla
Add cla
2023-07-18 17:42:00 +09:00
softsimon
b041a145b1 Merge pull request #3654 from learntheropes/patch-1
sign cla
2023-07-18 17:41:49 +09:00
softsimon
7046c3d6c3 Merge pull request #3935 from mempool/mononaut/lightning-justice
Add lightning justice page
2023-07-18 17:24:26 +09:00
softsimon
67f58a4491 Sorting by closing date descending 2023-07-18 17:19:14 +09:00
softsimon
d74e4b1876 Replacing loading text with spinner 2023-07-18 17:15:54 +09:00
softsimon
e757b74c87 Merge pull request #3990 from mempool/simon/removing-unused-fullrbf-code
Removing unused rbf frontend code
2023-07-18 17:06:01 +09:00
wiz
4b41730636 Merge branch 'master' into mononaut/lightning-justice 2023-07-18 16:49:49 +09:00
wiz
23ecf9cf41 Merge pull request #3993 from mempool/simon/backend-deps-18-07
Bumping backend deps
2023-07-18 16:19:28 +09:00
wiz
f8cfa35552 Merge pull request #3965 from mempool/nymkappa/network-switch-align
[network selection] fix some align issues
2023-07-18 15:53:41 +09:00
wiz
c1d0e802d9 Merge branch 'master' into nymkappa/network-switch-align 2023-07-18 15:40:57 +09:00
softsimon
bde7fad1c4 Bumping backend deps 2023-07-18 15:36:30 +09:00
softsimon
8007f5a3d3 Merge pull request #3992 from mempool/simon/angular-16
Angular 16
2023-07-18 15:10:49 +09:00
softsimon
83f955e469 Merge pull request #3969 from mempool/mononaut/ancestors-undefined
Fix tx.ancestors undefined bug
2023-07-18 15:10:17 +09:00
nymkappa
29c53a7852 [search bar] fix alignment issue 2023-07-18 15:01:30 +09:00
softsimon
3825a1c359 Trying downgrading cypress wait 2023-07-18 13:45:24 +09:00
softsimon
ac82a25fa2 Angular 16 2023-07-18 13:38:33 +09:00
softsimon
85e071049c Removing unused rbf frontend code 2023-07-18 11:42:13 +09:00
softsimon
ae22b7b444 Merge pull request #3989 from mempool/mononaut/fullrbf-list-toggle
Remove frontend FULL_RBF_ENABLED flag
2023-07-18 11:36:46 +09:00
softsimon
9aba4d4357 Merge pull request #3988 from mempool/mononaut/clock-width-fix
Fix clock horizontal scroll bug
2023-07-18 11:03:11 +09:00
Mononaut
17866f80bd Remove frontend FULL_RBF_ENABLED flag 2023-07-18 11:01:35 +09:00
Mononaut
5e46176c4e Fix clock horizontal scroll bug 2023-07-18 10:52:47 +09:00
Felipe Knorr Kuhn
69c54d7207 Merge branch 'master' into node_v20_to_matrix 2023-07-17 15:53:34 -07:00
wiz
2d4bc9dbd6 ops: Move electrs scripts to mempool/electrs repo 2023-07-16 17:50:36 +09:00
Mononaut
b6a6fcd4e2 Fix tx.ancestors undefined bug 2023-07-16 12:53:55 +09:00
nymkappa
cda6567c4c [network selector] fix rtl issue 2023-07-15 16:50:18 +09:00
nymkappa
a372b479b4 [network selector] improve align 2023-07-15 16:26:25 +09:00
softsimon
05f85c5201 Merge branch 'master' into nymkappa/clip-label-overflow 2023-07-12 10:57:11 +09:00
Felipe Knorr Kuhn
2570357bec Add node v20 to the test matrix 2023-07-11 00:21:00 -07:00
Mononaut
4ba552fe1b Add basic lightning justice page 2023-07-09 03:03:35 -04:00
junderw
ec918d57b2 Fix backend docker build for armv7 2023-07-08 23:03:03 -07:00
hunicus
c89b15fdbc Update mosp strings from tm to r 2023-06-27 12:37:04 -04:00
nymkappa
99af881cdd Merge branch 'master' into nymkappa/clip-label-overflow 2023-04-22 20:10:27 +02:00
Giovanni La Perna
71935e29c8 Update and rename giovannilaperna.txt to learntheropes.txt
change github username
2023-04-08 11:19:04 +02:00
Rex
123b853205 Add cla 2023-04-08 11:43:26 +08:00
Giovanni La Perna
bad73c1805 Create giovannilaperna.txt 2023-04-07 01:04:23 +02:00
nymkappa
7ab373ecac Clip overflowing labels in pool component on mobile 2023-03-29 15:09:38 +09:00
hunicus
6cc2f20638 Use href for enterprise links 2023-03-12 04:39:57 -04:00
wiz
730c1ae2d7 Merge branch 'master' into hunicus/manual-deployment-enterprise 2023-03-12 16:57:49 +09:00
hunicus
f493da4eac Generalize faq from linux to any server 2023-03-04 18:24:02 +09:00
hunicus
e7ad857cc9 Specify manual deployment support for enterprise sponsors [readme] 2023-03-04 18:24:02 +09:00
hunicus
f968faeaf9 Specify manual deployment support for enterprise sponsors [faq] 2023-03-04 18:24:02 +09:00
148 changed files with 9363 additions and 9416 deletions

View File

@@ -7,7 +7,8 @@ updates:
open-pull-requests-limit: 10
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
update-types:
["version-update:semver-major", "version-update:semver-patch"]
allow:
- dependency-type: "production"
@@ -18,7 +19,8 @@ updates:
open-pull-requests-limit: 10
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
update-types:
["version-update:semver-major", "version-update:semver-patch"]
allow:
- dependency-type: "production"
@@ -28,7 +30,8 @@ updates:
interval: weekly
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
update-types:
["version-update:semver-major", "version-update:semver-patch"]
- package-ecosystem: docker
directory: "/docker/frontend"
@@ -36,7 +39,8 @@ updates:
interval: weekly
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
update-types:
["version-update:semver-major", "version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: "/"
@@ -44,4 +48,5 @@ updates:
interval: weekly
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
update-types:
["version-update:semver-major", "version-update:semver-patch"]

View File

@@ -9,7 +9,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
node: ["16", "17", "18"]
node: ["16", "17", "18", "20"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
@@ -28,9 +28,7 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: Install 1.70.x Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: 1.70
uses: dtolnay/rust-toolchain@1.70
- name: Install
if: ${{ matrix.flavor == 'dev'}}
@@ -60,7 +58,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
node: ["16", "17", "18"]
node: ["16", "17", "18", "20"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
@@ -99,3 +97,6 @@ jobs:
- name: Build
run: npm run build
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -13,7 +13,7 @@ the terms of (at your option) either:
proxy statement published on <https://mempool.space/about>.
However, this copyright license does not include an implied right or license to
use our trademarks: The Mempool Open Source Project, mempool.space™, the
use our trademarks: The Mempool Open Source Project®, mempool.space™, the
mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal
Logo™, the mempool Square Logo™, and the mempool Blocks logo™ are registered
trademarks or trademarks of Mempool Space K.K in Japan, the United States,

View File

@@ -1,4 +1,4 @@
# 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/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4

1
backend/.dockerignore Normal file
View File

@@ -0,0 +1 @@
Dockerfile

View File

@@ -2,7 +2,7 @@
These instructions are mostly intended for developers.
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool does not provide support for custom setups.
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool only provides support for custom setups to [enterprise sponsors](https://mempool.space/enterprise).
See other ways to set up Mempool on [the main README](/../../#installation-methods).

View File

@@ -8,6 +8,7 @@
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000,
"CACHE_DIR": "./cache",
"CACHE_ENABLED": true,
"CLEAR_PROTECTION_MINUTES": 20,
"RECOMMENDED_FEE_PERCENTILE": 50,
"BLOCK_WEIGHT_UNITS": 4000000,

View File

@@ -12,16 +12,16 @@
"@babel/core": "^7.21.3",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~0.27.2",
"bitcoinjs-lib": "~6.1.0",
"axios": "~1.4.0",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.1.1",
"express": "~4.18.2",
"maxmind": "~4.3.8",
"mysql2": "~3.2.0",
"node-worker-threads-pool": "~1.5.1",
"maxmind": "~4.3.11",
"mysql2": "~3.5.2",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.7.4",
"typescript": "~4.9.3",
"ws": "~8.13.0"
},
"devDependencies": {
@@ -29,19 +29,28 @@
"@babel/core": "^7.21.3",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.15",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/ws": "~8.5.4",
"@types/ws": "~8.5.5",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.7.0",
"eslint-config-prettier": "^8.8.0",
"jest": "^29.5.0",
"prettier": "^2.8.4",
"ts-jest": "^29.0.5",
"prettier": "^3.0.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
"integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@@ -1490,7 +1499,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
"dev": true,
"bin": {
"napi": "scripts/index.js"
},
@@ -1548,6 +1556,64 @@
"node": ">= 8"
}
},
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/client/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@redis/graph": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -1795,9 +1861,9 @@
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
"integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -1865,9 +1931,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -2009,9 +2075,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -2068,9 +2134,9 @@
}
},
"node_modules/@typescript-eslint/utils/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -2255,12 +2321,13 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-jest": {
@@ -2449,9 +2516,9 @@
}
},
"node_modules/bitcoinjs-lib": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.1.tgz",
"integrity": "sha512-FYihfgTk29lt1eK2y48OtuarEDUnTprNBW3ctT8yHiOhvmeS3DzAVG6gI0VCvMkydz6UdlXlYNWIPqGD0SUYRQ==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz",
"integrity": "sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw==",
"dependencies": {
"@noble/hashes": "^1.2.0",
"bech32": "^2.0.0",
@@ -2710,6 +2777,14 @@
"node": ">=12"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -3670,6 +3745,14 @@
"is-property": "^1.0.2"
}
},
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
"engines": {
"node": ">= 4"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -5367,9 +5450,9 @@
}
},
"node_modules/jest-snapshot/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -5892,12 +5975,12 @@
}
},
"node_modules/maxmind": {
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.9.tgz",
"integrity": "sha512-rEfIxZ9M2P7CWQQzN5/LapCawpf2DLh+LWD/cA7lNfCbFL6dNJOKgtynp8QbRsxExutn7Ofz1P1tXEdL3gnukw==",
"version": "4.3.11",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.11.tgz",
"integrity": "sha512-tJDrKbUzN6PSA88tWgg0L2R4Ln00XwecYQJPFI+RvlF2k1sx6VQYtuQ1SVxm8+bw5tF7GWV4xyb+3/KyzEpPUw==",
"dependencies": {
"mmdb-lib": "2.0.2",
"tiny-lru": "10.3.0"
"tiny-lru": "11.0.1"
},
"engines": {
"node": ">=12",
@@ -6019,15 +6102,15 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.2.0.tgz",
"integrity": "sha512-0Vn6a9WSrq6fWwvPgrvIwnOCldiEcgbzapVRDAtDZ4cMTxN7pnGqCTx8EG32S/NYXl6AXkdO+9hV1tSIi/LigA==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz",
"integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru-cache": "^7.14.1",
"lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
@@ -6048,11 +6131,11 @@
}
},
"node_modules/mysql2/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
"engines": {
"node": ">=12"
"node": ">=16.14"
}
},
"node_modules/named-placeholders": {
@@ -6106,11 +6189,6 @@
"integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
"dev": true
},
"node_modules/node-worker-threads-pool": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/node-worker-threads-pool/-/node-worker-threads-pool-1.5.1.tgz",
"integrity": "sha512-7TXAhpMm+jO4MfESxYLtMGSnJWv+itdNHMdaFmeZuPXxwFGU90mtEB42BciUULXOUAxYBfXILAuvrSG3rQZ7mw=="
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -6176,17 +6254,17 @@
}
},
"node_modules/optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
"integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
"dev": true,
"dependencies": {
"@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
"type-check": "^0.4.0",
"word-wrap": "^1.2.3"
"type-check": "^0.4.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -6417,15 +6495,15 @@
}
},
"node_modules/prettier": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
"integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=10.13.0"
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
@@ -6482,6 +6560,11 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -6569,6 +6652,19 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"node_modules/redis": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
"integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.5.7",
"@redis/graph": "1.1.0",
"@redis/json": "1.0.4",
"@redis/search": "1.1.2",
"@redis/time-series": "1.0.4"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -6711,9 +6807,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
@@ -7050,9 +7146,9 @@
"dev": true
},
"node_modules/tiny-lru": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-10.3.0.tgz",
"integrity": "sha512-vTKRT2AEO1sViFDWAIzZVpV8KURCaMtnHa4RZB3XqtYLbrTO/fLDXKPEX9kVWq9u+nZREkwakbcmzGgvJm8QKA==",
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz",
"integrity": "sha512-iNgFugVuQgBKrqeO/mpiTTgmBsTP0WL6yeuLfLs/Ctf0pI/ixGqIRm8sDCwMcXGe9WWvt2sGXI5mNqZbValmJg==",
"engines": {
"node": ">=12"
}
@@ -7093,9 +7189,9 @@
}
},
"node_modules/ts-jest": {
"version": "29.0.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz",
"integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==",
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
"integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==",
"dev": true,
"dependencies": {
"bs-logger": "0.x",
@@ -7104,7 +7200,7 @@
"json5": "^2.2.3",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"semver": "^7.5.3",
"yargs-parser": "^21.0.1"
},
"bin": {
@@ -7118,7 +7214,7 @@
"@jest/types": "^29.0.0",
"babel-jest": "^29.0.0",
"jest": "^29.0.0",
"typescript": ">=4.3"
"typescript": ">=4.3 <6"
},
"peerDependenciesMeta": {
"@babel/core": {
@@ -7148,9 +7244,9 @@
}
},
"node_modules/ts-jest/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -7283,9 +7379,9 @@
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -7405,15 +7501,6 @@
"node": ">= 8"
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -7568,7 +7655,7 @@
},
"rust-gbt": {
"name": "gbt",
"version": "0.1.0",
"version": "3.0.0-dev",
"hasInstallScript": true,
"dependencies": {
"@napi-rs/cli": "^2.16.1"
@@ -7579,6 +7666,12 @@
}
},
"dependencies": {
"@aashutoshrathi/word-wrap": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
"integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
"dev": true
},
"@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@@ -8666,8 +8759,7 @@
"@napi-rs/cli": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.1.tgz",
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA==",
"dev": true
"integrity": "sha512-L0Gr5iEQIDEbvWdDr1HUaBOxBSHL1VZhWSk1oryawoT8qJIY+KGfLFelU+Qma64ivCPbxYpkfPoKYVG3rcoGIA=="
},
"@noble/hashes": {
"version": "1.3.0",
@@ -8700,6 +8792,53 @@
"fastq": "^1.6.0"
}
},
"@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"requires": {}
},
"@redis/client": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
"requires": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"dependencies": {
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
},
"@redis/graph": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
"requires": {}
},
"@redis/json": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
"requires": {}
},
"@redis/search": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
"requires": {}
},
"@redis/time-series": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
"requires": {}
},
"@sinclair/typebox": {
"version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -8947,9 +9086,9 @@
"dev": true
},
"@types/ws": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
"integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -8998,9 +9137,9 @@
}
},
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -9079,9 +9218,9 @@
}
},
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -9121,9 +9260,9 @@
}
},
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -9258,12 +9397,13 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"requires": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"babel-jest": {
@@ -9409,9 +9549,9 @@
"integrity": "sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA=="
},
"bitcoinjs-lib": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.1.tgz",
"integrity": "sha512-FYihfgTk29lt1eK2y48OtuarEDUnTprNBW3ctT8yHiOhvmeS3DzAVG6gI0VCvMkydz6UdlXlYNWIPqGD0SUYRQ==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.3.tgz",
"integrity": "sha512-TYXs/Qf+GNk2nnsB9HrXWqzFuEgCg0Gx+v3UW3B8VuceFHXVvhT+7hRnTSvwkX0i8rz2rtujeU6gFaDcFqYFDw==",
"requires": {
"@noble/hashes": "^1.2.0",
"bech32": "^2.0.0",
@@ -9599,6 +9739,11 @@
"wrap-ansi": "^7.0.0"
}
},
"cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -10327,6 +10472,11 @@
"is-property": "^1.0.2"
}
},
"generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="
},
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -11577,9 +11727,9 @@
}
},
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -11973,12 +12123,12 @@
}
},
"maxmind": {
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.9.tgz",
"integrity": "sha512-rEfIxZ9M2P7CWQQzN5/LapCawpf2DLh+LWD/cA7lNfCbFL6dNJOKgtynp8QbRsxExutn7Ofz1P1tXEdL3gnukw==",
"version": "4.3.11",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.11.tgz",
"integrity": "sha512-tJDrKbUzN6PSA88tWgg0L2R4Ln00XwecYQJPFI+RvlF2k1sx6VQYtuQ1SVxm8+bw5tF7GWV4xyb+3/KyzEpPUw==",
"requires": {
"mmdb-lib": "2.0.2",
"tiny-lru": "10.3.0"
"tiny-lru": "11.0.1"
}
},
"media-typer": {
@@ -12062,15 +12212,15 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.2.0.tgz",
"integrity": "sha512-0Vn6a9WSrq6fWwvPgrvIwnOCldiEcgbzapVRDAtDZ4cMTxN7pnGqCTx8EG32S/NYXl6AXkdO+9hV1tSIi/LigA==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz",
"integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==",
"requires": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru-cache": "^7.14.1",
"lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
@@ -12085,9 +12235,9 @@
}
},
"lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA=="
}
}
},
@@ -12135,11 +12285,6 @@
"integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
"dev": true
},
"node-worker-threads-pool": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/node-worker-threads-pool/-/node-worker-threads-pool-1.5.1.tgz",
"integrity": "sha512-7TXAhpMm+jO4MfESxYLtMGSnJWv+itdNHMdaFmeZuPXxwFGU90mtEB42BciUULXOUAxYBfXILAuvrSG3rQZ7mw=="
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -12187,17 +12332,17 @@
}
},
"optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
"integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
"dev": true,
"requires": {
"@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
"type-check": "^0.4.0",
"word-wrap": "^1.2.3"
"type-check": "^0.4.0"
}
},
"p-limit": {
@@ -12358,9 +12503,9 @@
"dev": true
},
"prettier": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
"integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
"dev": true
},
"pretty-format": {
@@ -12401,6 +12546,11 @@
"ipaddr.js": "1.9.1"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -12449,6 +12599,19 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"redis": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
"integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
"requires": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.5.7",
"@redis/graph": "1.1.0",
"@redis/json": "1.0.4",
"@redis/search": "1.1.2",
"@redis/time-series": "1.0.4"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -12536,9 +12699,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true
},
"send": {
@@ -12801,9 +12964,9 @@
"dev": true
},
"tiny-lru": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-10.3.0.tgz",
"integrity": "sha512-vTKRT2AEO1sViFDWAIzZVpV8KURCaMtnHa4RZB3XqtYLbrTO/fLDXKPEX9kVWq9u+nZREkwakbcmzGgvJm8QKA=="
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz",
"integrity": "sha512-iNgFugVuQgBKrqeO/mpiTTgmBsTP0WL6yeuLfLs/Ctf0pI/ixGqIRm8sDCwMcXGe9WWvt2sGXI5mNqZbValmJg=="
},
"tmpl": {
"version": "1.0.5",
@@ -12832,9 +12995,9 @@
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"ts-jest": {
"version": "29.0.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz",
"integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==",
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
"integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==",
"dev": true,
"requires": {
"bs-logger": "0.x",
@@ -12843,7 +13006,7 @@
"json5": "^2.2.3",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"semver": "^7.5.3",
"yargs-parser": "^21.0.1"
},
"dependencies": {
@@ -12857,9 +13020,9 @@
}
},
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -12945,9 +13108,9 @@
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ=="
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="
},
"unpipe": {
"version": "1.0.0",
@@ -13026,12 +13189,6 @@
"isexe": "^2.0.0"
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true
},
"wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@@ -40,33 +40,33 @@
"@babel/core": "^7.21.3",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~0.27.2",
"bitcoinjs-lib": "~6.1.0",
"axios": "~1.4.0",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.1.1",
"express": "~4.18.2",
"maxmind": "~4.3.8",
"mysql2": "~3.2.0",
"node-worker-threads-pool": "~1.5.1",
"maxmind": "~4.3.11",
"mysql2": "~3.5.2",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.7.4",
"typescript": "~4.9.3",
"ws": "~8.13.0"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.21.3",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.15",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/ws": "~8.5.4",
"@types/ws": "~8.5.5",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.7.0",
"eslint-config-prettier": "^8.8.0",
"jest": "^29.5.0",
"prettier": "^2.8.4",
"ts-jest": "^29.0.5",
"prettier": "^3.0.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1"
}
}

View File

@@ -10,6 +10,7 @@
"AUTOMATIC_BLOCK_REINDEXING": false,
"POLL_RATE_MS": 3,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CACHE_ENABLED": true,
"CLEAR_PROTECTION_MINUTES": 4,
"RECOMMENDED_FEE_PERCENTILE": 5,
"BLOCK_WEIGHT_UNITS": 6,
@@ -127,5 +128,9 @@
"AUDIT": false,
"AUDIT_START_HEIGHT": 774000,
"SERVERS": []
},
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
}
}

View File

@@ -23,6 +23,7 @@ describe('Mempool Backend Config', () => {
AUTOMATIC_BLOCK_REINDEXING: false,
POLL_RATE_MS: 2000,
CACHE_DIR: './cache',
CACHE_ENABLED: true,
CLEAR_PROTECTION_MINUTES: 20,
RECOMMENDED_FEE_PERCENTILE: 50,
BLOCK_WEIGHT_UNITS: 4000000,
@@ -127,6 +128,11 @@ describe('Mempool Backend Config', () => {
AUDIT_START_HEIGHT: 774000,
SERVERS: []
});
expect(config.REDIS).toStrictEqual({
ENABLED: false,
UNIX_SOCKET_PATH: ''
});
});
});
@@ -160,6 +166,8 @@ describe('Mempool Backend Config', () => {
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
expect(config.REDIS).toStrictEqual(fixture.REDIS);
});
});
@@ -173,12 +181,12 @@ describe('Mempool Backend Config', () => {
// 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;
continue;
}
switch (typeof value) {
case 'object': {
if (Array.isArray(value)) {
return;
continue;
} else {
parseJson(value, key);
}

View File

@@ -15,7 +15,7 @@ class Audit {
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const isCensored = {}; // missing, without excuse
const isDisplaced = {};
let displacedWeight = 0;
@@ -36,8 +36,9 @@ class Audit {
// look for transactions that were expected in the template, but missing from the mined block
for (const txid of projectedBlocks[0].transactionIds) {
if (!inBlock[txid]) {
if (rbfCache.isFullRbf(txid)) {
fullrbf.push(txid);
// allow missing transactions which either belong to a full rbf tree, or conflict with any transaction in the mined block
if (rbfCache.has(txid) && (rbfCache.isFullRbf(txid) || rbfCache.anyInSameTree(txid, (tx) => inBlock[tx.txid]))) {
rbf.push(txid);
} else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
// tx is recent, may have reached the miner too late for inclusion
fresh.push(txid);
@@ -98,8 +99,8 @@ class Audit {
if (inTemplate[tx.txid]) {
matches.push(tx.txid);
} else {
if (rbfCache.isFullRbf(tx.txid)) {
fullrbf.push(tx.txid);
if (rbfCache.has(tx.txid)) {
rbf.push(tx.txid);
} else if (!isDisplaced[tx.txid]) {
added.push(tx.txid);
}
@@ -147,7 +148,7 @@ class Audit {
added,
fresh,
sigop: [],
fullrbf,
fullrbf: rbf,
score,
similarity,
};

View File

@@ -7,7 +7,6 @@ import { SocksProxyAgent } from 'socks-proxy-agent';
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
import { Common } from '../common';
import { BlockExtended } from '../../mempool.interfaces';
import { StaticPool } from 'node-worker-threads-pool';
import backendInfo from '../backend-info';
import logger from '../../logger';
@@ -31,10 +30,6 @@ class Bisq {
private priceUpdateCallbackFunction: ((price: number) => void) | undefined;
private topDirectoryWatcher: fs.FSWatcher | undefined;
private subdirectoryWatcher: fs.FSWatcher | undefined;
private jsonParsePool = new StaticPool({
size: 4,
task: (blob: string) => JSON.parse(blob),
});
constructor() {}

View File

@@ -3,10 +3,12 @@ import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getMempoolTransactions(lastTxid: string);
$getTransactionHex(txId: string): Promise<string>;
$getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>;
$getTxIdsForBlock(hash: string): Promise<string[]>;
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]>;
$getBlockHash(height: number): Promise<string>;
$getBlockHeader(hash: string): Promise<string>;
$getBlock(hash: string): Promise<IEsploraApi.Block>;
@@ -14,6 +16,8 @@ export interface AbstractBitcoinApi {
$getAddress(address: string): Promise<IEsploraApi.Address>;
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$getAddressPrefix(prefix: string): string[];
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;

View File

@@ -5,6 +5,7 @@ import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import mempool from '../mempool';
import { TransactionExtended } from '../../mempool.interfaces';
import transactionUtils from '../transaction-utils';
class BitcoinApi implements AbstractBitcoinApi {
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
@@ -59,9 +60,20 @@ class BitcoinApi implements AbstractBitcoinApi {
});
}
$getTransactionHex(txId: string): Promise<string> {
return this.$getRawTransaction(txId, true)
.then((tx) => tx.hex || '');
$getMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
return Promise.resolve([]);
}
async $getTransactionHex(txId: string): Promise<string> {
const txInMempool = mempool.getMempool()[txId];
if (txInMempool && txInMempool.hex) {
return txInMempool.hex;
}
return this.bitcoindClient.getRawTransaction(txId, true)
.then((transaction: IBitcoinApi.Transaction) => {
return transaction.hex;
});
}
$getBlockHeightTip(): Promise<number> {
@@ -77,6 +89,10 @@ class BitcoinApi implements AbstractBitcoinApi {
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
}
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.');
}
$getRawBlock(hash: string): Promise<Buffer> {
return this.bitcoindClient.getBlock(hash, 0)
.then((raw: string) => Buffer.from(raw, "hex"));
@@ -108,6 +124,14 @@ class BitcoinApi implements AbstractBitcoinApi {
throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
}
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
throw new Error('Method getScriptHash not supported by the Bitcoin RPC API.');
}
$getScriptHashTransactions(scripthash: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getScriptHashTransactions not supported by the Bitcoin RPC API.');
}
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return this.bitcoindClient.getRawMemPool();
}
@@ -193,7 +217,7 @@ class BitcoinApi implements AbstractBitcoinApi {
scriptpubkey: vout.scriptPubKey.hex,
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
: vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
scriptpubkey_asm: vout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
};
});
@@ -203,7 +227,7 @@ class BitcoinApi implements AbstractBitcoinApi {
is_coinbase: !!vin.coinbase,
prevout: null,
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '',
scriptsig_asm: vin.scriptSig && transactionUtils.convertScriptSigAsm(vin.scriptSig.hex) || '',
sequence: vin.sequence,
txid: vin.txid || '',
vout: vin.vout || 0,
@@ -275,7 +299,7 @@ class BitcoinApi implements AbstractBitcoinApi {
}
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
transactionUtils.addInnerScriptsToVin(vin);
}
return transaction;
}
@@ -314,7 +338,7 @@ class BitcoinApi implements AbstractBitcoinApi {
}
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
this.addInnerScriptsToVin(transaction.vin[i]);
transactionUtils.addInnerScriptsToVin(transaction.vin[i]);
totalIn += innerTx.vout[transaction.vin[i].vout].value;
}
if (lazyPrevouts && transaction.vin.length > 12) {
@@ -326,122 +350,6 @@ class BitcoinApi implements AbstractBitcoinApi {
return transaction;
}
private convertScriptSigAsm(hex: string): string {
const buf = Buffer.from(hex, 'hex');
const b: string[] = [];
let i = 0;
while (i < buf.length) {
const op = buf[i];
if (op >= 0x01 && op <= 0x4e) {
i++;
let push: number;
if (op === 0x4c) {
push = buf.readUInt8(i);
b.push('OP_PUSHDATA1');
i += 1;
} else if (op === 0x4d) {
push = buf.readUInt16LE(i);
b.push('OP_PUSHDATA2');
i += 2;
} else if (op === 0x4e) {
push = buf.readUInt32LE(i);
b.push('OP_PUSHDATA4');
i += 4;
} else {
push = op;
b.push('OP_PUSHBYTES_' + push);
}
const data = buf.slice(i, i + push);
if (data.length !== push) {
break;
}
b.push(data.toString('hex'));
i += data.length;
} else {
if (op === 0x00) {
b.push('OP_0');
} else if (op === 0x4f) {
b.push('OP_PUSHNUM_NEG1');
} else if (op === 0xb1) {
b.push('OP_CLTV');
} else if (op === 0xb2) {
b.push('OP_CSV');
} else if (op === 0xba) {
b.push('OP_CHECKSIGADD');
} else {
const opcode = bitcoinjs.script.toASM([ op ]);
if (opcode && op < 0xfd) {
if (/^OP_(\d+)$/.test(opcode)) {
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
} else {
b.push(opcode);
}
} else {
b.push('OP_RETURN_' + op);
}
}
i += 1;
}
}
return b.join(' ');
}
private addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
if (!vin.prevout) {
return;
}
if (vin.prevout.scriptpubkey_type === 'p2sh') {
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
if (vin.witness && vin.witness.length > 2) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
}
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
const witnessScript = this.witnessToP2TRScript(vin.witness);
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;

View File

@@ -6,7 +6,7 @@ import websocketHandler from '../websocket-handler';
import mempool from '../mempool';
import feeApi from '../fee-api';
import mempoolBlocks from '../mempool-blocks';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin-api-factory';
import bitcoinApi from './bitcoin-api-factory';
import { Common } from '../common';
import backendInfo from '../backend-info';
import transactionUtils from '../transaction-utils';
@@ -121,6 +121,8 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash', this.getScriptHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash/txs', this.getScriptHashTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
;
}
@@ -481,7 +483,7 @@ class BitcoinRoutes {
returnBlocks.push(localBlock);
nextHash = localBlock.previousblockhash;
} else {
const block = await bitcoinCoreApi.$getBlock(nextHash);
const block = await bitcoinApi.$getBlock(nextHash);
returnBlocks.push(block);
nextHash = block.previousblockhash;
}
@@ -567,6 +569,45 @@ class BitcoinRoutes {
}
}
private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
try {
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
return res.status(413).send(e instanceof Error ? e.message : e);
}
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
try {
let lastTxId: string = '';
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
lastTxId = req.query.after_txid;
}
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
res.status(413).send(e instanceof Error ? e.message : e);
return;
}
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getAddressPrefix(req: Request, res: Response) {
try {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);

View File

@@ -126,6 +126,77 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
}
}
async $getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
try {
const balance = await this.electrumClient.blockchainScripthash_getBalance(scripthash);
let history = memoryCache.get<IElectrumApi.ScriptHashHistory[]>('Scripthash_getHistory', scripthash);
if (!history) {
history = await this.electrumClient.blockchainScripthash_getHistory(scripthash);
memoryCache.set('Scripthash_getHistory', scripthash, history, 2);
}
const unconfirmed = history ? history.filter((h) => h.fee).length : 0;
return {
'scripthash': scripthash,
'chain_stats': {
'funded_txo_count': 0,
'funded_txo_sum': balance.confirmed ? balance.confirmed : 0,
'spent_txo_count': 0,
'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0,
'tx_count': (history?.length || 0) - unconfirmed,
},
'mempool_stats': {
'funded_txo_count': 0,
'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0,
'spent_txo_count': 0,
'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0,
'tx_count': unconfirmed,
},
'electrum': true,
};
} catch (e: any) {
throw new Error(typeof e === 'string' ? e : e && e.message || e);
}
}
async $getScriptHashTransactions(scripthash: string, lastSeenTxId?: string): Promise<IEsploraApi.Transaction[]> {
try {
loadingIndicators.setProgress('address-' + scripthash, 0);
const transactions: IEsploraApi.Transaction[] = [];
let history = memoryCache.get<IElectrumApi.ScriptHashHistory[]>('Scripthash_getHistory', scripthash);
if (!history) {
history = await this.electrumClient.blockchainScripthash_getHistory(scripthash);
memoryCache.set('Scripthash_getHistory', scripthash, history, 2);
}
if (!history) {
throw new Error('failed to get scripthash history');
}
history.sort((a, b) => (b.height || 9999999) - (a.height || 9999999));
let startingIndex = 0;
if (lastSeenTxId) {
const pos = history.findIndex((historicalTx) => historicalTx.tx_hash === lastSeenTxId);
if (pos) {
startingIndex = pos + 1;
}
}
const endIndex = Math.min(startingIndex + 10, history.length);
for (let i = startingIndex; i < endIndex; i++) {
const tx = await this.$getRawTransaction(history[i].tx_hash, false, true);
transactions.push(tx);
loadingIndicators.setProgress('address-' + scripthash, (i + 1) / endIndex * 100);
}
return transactions;
} catch (e: any) {
loadingIndicators.setProgress('address-' + scripthash, 100);
throw new Error(typeof e === 'string' ? e : e && e.message || e);
}
}
private $getScriptHashBalance(scriptHash: string): Promise<IElectrumApi.ScriptHashBalance> {
return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash));
}

View File

@@ -99,6 +99,13 @@ export namespace IEsploraApi {
electrum?: boolean;
}
export interface ScriptHash {
scripthash: string;
chain_stats: ChainStats;
mempool_stats: MempoolStats;
electrum?: boolean;
}
export interface ChainStats {
funded_txo_count: number;
funded_txo_sum: number;

View File

@@ -69,6 +69,10 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
}
async $getMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
}
$getTransactionHex(txId: string): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
}
@@ -85,6 +89,10 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
}
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs');
}
$getBlockHash(height: number): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
}
@@ -110,6 +118,14 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method getAddressTransactions not implemented.');
}
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
throw new Error('Method getScriptHash not implemented.');
}
$getScriptHashTransactions(scripthash: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getScriptHashTransactions not implemented.');
}
$getAddressPrefix(prefix: string): string[] {
throw new Error('Method not implemented.');
}

View File

@@ -26,6 +26,8 @@ import PricesRepository from '../repositories/PricesRepository';
import priceUpdater from '../tasks/price-updater';
import chainTips from './chain-tips';
import websocketHandler from './websocket-handler';
import redisCache from './redis-cache';
import rbfCache from './rbf-cache';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -70,6 +72,9 @@ class Blocks {
* @param blockHash
* @param blockHeight
* @param onlyCoinbase - Set to true if you only need the coinbase transaction
* @param txIds - optional ordered list of transaction ids if already known
* @param quiet - don't print non-essential logs
* @param addMempoolData - calculate sigops etc
* @returns Promise<TransactionExtended[]>
*/
private async $getTransactionsExtended(
@@ -80,62 +85,98 @@ class Blocks {
quiet: boolean = false,
addMempoolData: boolean = false,
): Promise<TransactionExtended[]> {
const transactions: TransactionExtended[] = [];
const isEsplora = config.MEMPOOL.BACKEND === 'esplora';
const transactionMap: { [txid: string]: TransactionExtended } = {};
if (!txIds) {
txIds = await bitcoinApi.$getTxIdsForBlock(blockHash);
}
const mempool = memPool.getMempool();
let transactionsFound = 0;
let transactionsFetched = 0;
let foundInMempool = 0;
let totalFound = 0;
for (let i = 0; i < txIds.length; i++) {
if (mempool[txIds[i]]) {
// We update blocks before the mempool (index.ts), therefore we can
// optimize here by directly fetching txs in the "outdated" mempool
transactions.push(mempool[txIds[i]]);
transactionsFound++;
} else if (config.MEMPOOL.BACKEND === 'esplora' || !memPool.hasPriority() || i === 0) {
// Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...)
if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam
logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
}
try {
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData);
transactions.push(tx);
transactionsFetched++;
} catch (e) {
try {
if (config.MEMPOOL.BACKEND === 'esplora') {
// Try again with core
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true, addMempoolData);
transactions.push(tx);
transactionsFetched++;
} else {
throw e;
}
} catch (e) {
if (i === 0) {
const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
throw new Error(msg);
} else {
logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
// Copy existing transactions from the mempool
if (!onlyCoinbase) {
for (const txid of txIds) {
if (mempool[txid]) {
transactionMap[txid] = mempool[txid];
foundInMempool++;
totalFound++;
}
}
}
if (onlyCoinbase === true) {
break; // Fetch the first transaction and exit
if (onlyCoinbase) {
try {
const coinbase = await transactionUtils.$getTransactionExtendedRetry(txIds[0], false, false, false, addMempoolData);
if (coinbase && coinbase.vin[0].is_coinbase) {
return [coinbase];
} else {
const msg = `Expected a coinbase tx, but the backend API returned something else`;
logger.err(msg);
throw new Error(msg);
}
} catch (e) {
const msg = `Cannot fetch coinbase tx ${txIds[0]}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
throw new Error(msg);
}
}
// Fetch remaining txs in bulk
if (isEsplora && (txIds.length - totalFound > 500)) {
try {
const rawTransactions = await bitcoinApi.$getTxsForBlock(blockHash);
for (const tx of rawTransactions) {
if (!transactionMap[tx.txid]) {
transactionMap[tx.txid] = addMempoolData ? transactionUtils.extendMempoolTransaction(tx) : transactionUtils.extendTransaction(tx);
totalFound++;
}
}
} catch (e) {
logger.err(`Cannot fetch bulk txs for block ${blockHash}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
// Fetch remaining txs individually
for (const txid of txIds.filter(txid => !transactionMap[txid])) {
if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
}
try {
const tx = await transactionUtils.$getTransactionExtendedRetry(txid, false, false, false, addMempoolData);
transactionMap[txid] = tx;
totalFound++;
} catch (e) {
const msg = `Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
throw new Error(msg);
}
}
if (!quiet) {
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
logger.debug(`${foundInMempool} of ${txIds.length} found in mempool. ${totalFound - foundInMempool} fetched through backend service.`);
}
return transactions;
// Require the first transaction to be a coinbase
const coinbase = transactionMap[txIds[0]];
if (!coinbase || !coinbase.vin[0].is_coinbase) {
const msg = `Expected first tx in a block to be a coinbase, but found something else`;
logger.err(msg);
throw new Error(msg);
}
// Require all transactions to be present
// (we should have thrown an error already if a tx request failed)
if (txIds.some(txid => !transactionMap[txid])) {
const msg = `Failed to fetch ${txIds.length - totalFound} transactions from block`;
logger.err(msg);
throw new Error(msg);
}
// Return list of transactions, preserving block order
return txIds.map(txid => transactionMap[txid]);
}
/**
@@ -171,7 +212,9 @@ 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);
if (!isFinite(Number(tx.fee))) {
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
}
});
return block;
}
@@ -376,8 +419,8 @@ class Blocks {
let newlyIndexed = 0;
let totalIndexed = indexedBlockSummariesHashesArray.length;
let indexedThisRun = 0;
let timer = new Date().getTime() / 1000;
const startedAt = new Date().getTime() / 1000;
let timer = Date.now() / 1000;
const startedAt = Date.now() / 1000;
for (const block of indexedBlocks) {
if (indexedBlockSummariesHashes[block.hash] === true) {
@@ -385,17 +428,24 @@ class Blocks {
}
// Logging
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = indexedThisRun / elapsedSeconds;
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000;
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now() / 1000;
indexedThisRun = 0;
}
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
} else {
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
}
// Logging
indexedThisRun++;
@@ -434,18 +484,18 @@ class Blocks {
// Logging
let count = 0;
let countThisRun = 0;
let timer = new Date().getTime() / 1000;
const startedAt = new Date().getTime() / 1000;
let timer = Date.now() / 1000;
const startedAt = Date.now() / 1000;
for (const height of unindexedBlockHeights) {
// Logging
const hash = await bitcoinApi.$getBlockHash(height);
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = (countThisRun / elapsedSeconds);
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = countThisRun / elapsedSeconds;
const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100;
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000;
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`);
timer = Date.now() / 1000;
countThisRun = 0;
}
@@ -524,8 +574,8 @@ class Blocks {
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
let indexedThisRun = 0;
let newlyIndexed = 0;
const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000;
const startedAt = Date.now() / 1000;
let timer = Date.now() / 1000;
while (currentBlockHeight >= lastBlockToIndex) {
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
@@ -545,18 +595,18 @@ class Blocks {
}
++indexedThisRun;
++totalIndexed;
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = indexedThisRun / elapsedSeconds;
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000;
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('block-indexing', progress, false);
}
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
@@ -613,7 +663,7 @@ class Blocks {
const heightDiff = blockHeightTip % 2016;
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 bitcoinApi.$getBlock(blockHash);
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentDifficulty = block.difficulty;
@@ -621,7 +671,7 @@ class Blocks {
if (blockHeightTip >= 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 bitcoinApi.$getBlock(previousPeriodBlockHash);
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
logger.debug(`Initial difficulty adjustment data set.`);
@@ -647,14 +697,14 @@ class Blocks {
const block = BitcoinApi.convertBlock(verboseBlock);
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
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;
}
// 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.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
@@ -763,10 +813,18 @@ class Blocks {
if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
}
if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
if (config.MEMPOOL.CACHE_ENABLED && !memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
diskCache.$saveCacheToDisk();
}
// Update Redis cache
if (config.REDIS.ENABLED) {
await redisCache.$updateBlocks(this.blocks);
await redisCache.$updateBlockSummaries(this.blockSummaries);
await redisCache.$removeTransactions(txIds);
await rbfCache.updateCache();
}
handledBlocks++;
}
@@ -811,7 +869,7 @@ class Blocks {
}
const blockHash = await bitcoinApi.$getBlockHash(height);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
@@ -823,7 +881,7 @@ class Blocks {
}
public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
@@ -848,7 +906,7 @@ class Blocks {
}
// Bitcoin network, add our custom data on top
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
if (block.stale) {
return await this.$indexStaleBlock(hash);
} else {
@@ -877,7 +935,7 @@ class Blocks {
let height = blockHeight;
let summary: BlockSummary;
if (cpfpSummary) {
if (cpfpSummary && !Common.isLiquid()) {
summary = {
id: hash,
transactions: cpfpSummary.transactions.map(tx => {
@@ -891,10 +949,15 @@ class Blocks {
}),
};
} else {
// Call Core RPC
const block = await bitcoinClient.getBlock(hash, 2);
summary = this.summarizeBlock(block);
height = block.height;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(hash, txs);
} else {
// Call Core RPC
const block = await bitcoinClient.getBlock(hash, 2);
summary = this.summarizeBlock(block);
height = block.height;
}
}
if (height == null) {
const block = await bitcoinApi.$getBlock(hash);
@@ -1017,8 +1080,17 @@ class Blocks {
if (Common.blocksSummariesIndexingEnabled() && cleanBlock.fee_amt_percentiles === null) {
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
if (cleanBlock.fee_amt_percentiles === null) {
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
const summary = this.summarizeBlock(block);
let summary;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
} else {
// Call Core RPC
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
summary = this.summarizeBlock(block);
}
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
}
@@ -1078,19 +1150,29 @@ class Blocks {
return this.currentBlockHeight;
}
public async $indexCPFP(hash: string, height: number): Promise<void> {
const block = await bitcoinClient.getBlock(hash, 2);
const transactions = block.tx.map(tx => {
tx.fee *= 100_000_000;
return tx;
});
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> {
let transactions = txs;
if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') {
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
}
if (!transactions) {
const block = await bitcoinClient.getBlock(hash, 2);
transactions = block.tx.map(tx => {
tx.fee *= 100_000_000;
return tx;
});
}
}
const summary = Common.calculateCpfp(height, transactions);
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
await this.$saveCpfp(hash, height, summary);
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
return summary;
}
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {

View File

@@ -29,7 +29,7 @@ class DiskCache {
};
constructor() {
if (!cluster.isPrimary) {
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
return;
}
process.on('SIGINT', (e) => {
@@ -39,7 +39,7 @@ class DiskCache {
}
async $saveCacheToDisk(sync: boolean = false): Promise<void> {
if (!cluster.isPrimary) {
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
return;
}
if (this.isWritingCache) {
@@ -175,10 +175,11 @@ class DiskCache {
}
async $loadMempoolCache(): Promise<void> {
if (!fs.existsSync(DiskCache.FILE_NAME)) {
if (!config.MEMPOOL.CACHE_ENABLED || !fs.existsSync(DiskCache.FILE_NAME)) {
return;
}
try {
const start = Date.now();
let data: any = {};
const cacheData = fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
if (cacheData) {
@@ -220,6 +221,8 @@ class DiskCache {
}
}
logger.info(`Loaded mempool from disk cache in ${Date.now() - start} ms`);
await memPool.$setMempool(data.mempool);
if (!this.ignoreBlocksCache) {
blocks.setBlocks(data.blocks);

View File

@@ -80,7 +80,7 @@ class ChannelsApi {
public async $searchChannelsById(search: string): Promise<any[]> {
try {
const searchStripped = search.replace('%', '') + '%';
const searchStripped = search.replace(/[^0-9x]/g, '') + '%';
const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`;
const [rows]: any = await DB.query(query, [searchStripped, searchStripped]);
return rows;
@@ -117,6 +117,26 @@ class ChannelsApi {
}
}
public async $getPenaltyClosedChannels(): Promise<any[]> {
try {
const query = `
SELECT n1.alias AS alias_left,
n2.alias AS alias_right,
channels.*
FROM channels
LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key
LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key
WHERE channels.status = 2 AND channels.closing_reason = 3
ORDER BY closing_date DESC
`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('$getPenaltyClosedChannels error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getUnresolvedClosedChannels(): Promise<any[]> {
try {
const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason = 2 AND closing_resolved = 0 AND closing_transaction_id != ''`;

View File

@@ -11,6 +11,7 @@ class ChannelsRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/search/:search', this.$searchChannelsById)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels', this.$getChannelsForNode)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/penalties', this.$getPenaltyClosedChannels)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo', this.$getAllChannelsGeo)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo/:publicKey', this.$getAllChannelsGeo)
;
@@ -108,6 +109,18 @@ class ChannelsRoutes {
}
}
private async $getPenaltyClosedChannels(req: Request, res: Response): Promise<void> {
try {
const channels = await channelsApi.$getPenaltyClosedChannels();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getAllChannelsGeo(req: Request, res: Response) {
try {
const style: string = typeof req.query.style === 'string' ? req.query.style : '';

View File

@@ -217,7 +217,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
return {
channel_id: Common.channelShortIdToIntegerId(clChannelA.short_channel_id),
capacity: clChannelA.satoshis,
capacity: (clChannelA.amount_msat / 1000).toString(),
last_update: lastUpdate,
node1_policy: convertPolicy(clChannelA),
node2_policy: convertPolicy(clChannelB),
@@ -241,7 +241,7 @@ async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Cha
return {
channel_id: Common.channelShortIdToIntegerId(clChannel.short_channel_id),
capacity: clChannel.satoshis,
capacity: (clChannel.amount_msat / 1000).toString(),
last_update: clChannel.last_update ?? 0,
node1_policy: convertPolicy(clChannel),
node2_policy: null,
@@ -257,8 +257,8 @@ async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Cha
function convertPolicy(clChannel: any): ILightningApi.RoutingPolicy {
return {
time_lock_delta: clChannel.delay,
min_htlc: clChannel.htlc_minimum_msat.slice(0, -4),
max_htlc_msat: clChannel.htlc_maximum_msat.slice(0, -4),
min_htlc: clChannel.htlc_minimum_msat.toString(),
max_htlc_msat: clChannel.htlc_maximum_msat.toString(),
fee_base_msat: clChannel.base_fee_millisatoshi,
fee_rate_milli_msat: clChannel.fee_per_millionth,
disabled: !clChannel.active,

View File

@@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import redisCache from './redis-cache';
class Mempool {
private inSync: boolean = false;
@@ -85,14 +86,25 @@ class Mempool {
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
this.mempoolCache = mempoolData;
let count = 0;
const redisTimer = Date.now();
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
logger.debug(`Migrating ${Object.keys(this.mempoolCache).length} transactions from disk cache to Redis cache`);
}
for (const txid of Object.keys(this.mempoolCache)) {
if (this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) {
if (!this.mempoolCache[txid].sigops || this.mempoolCache[txid].effectiveFeePerVsize == null) {
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
}
if (this.mempoolCache[txid].order == null) {
this.mempoolCache[txid].order = transactionUtils.txidToOrdering(txid);
}
count++;
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
await redisCache.$addTransaction(this.mempoolCache[txid]);
}
}
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
await redisCache.$flushTransactions();
logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
}
if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []);
@@ -103,6 +115,44 @@ class Mempool {
this.addToSpendMap(Object.values(this.mempoolCache));
}
public async $reloadMempool(expectedCount: number): Promise<MempoolTransactionExtended[]> {
let count = 0;
let done = false;
let last_txid;
const newTransactions: MempoolTransactionExtended[] = [];
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) {
try {
const result = await bitcoinApi.$getMempoolTransactions(last_txid);
if (result) {
for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
if (!this.mempoolCache[extendedTransaction.txid]) {
newTransactions.push(extendedTransaction);
this.mempoolCache[extendedTransaction.txid] = extendedTransaction;
}
count++;
}
logger.info(`Fetched ${count} of ${expectedCount} mempool transactions from esplora`);
if (result.length > 0) {
last_txid = result[result.length - 1].txid;
} else {
done = true;
}
if (Math.floor((count / expectedCount) * 100) < 100) {
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
}
} else {
done = true;
}
} catch(err) {
logger.err('failed to fetch bulk mempool transactions from esplora');
}
}
logger.info(`Done inserting loaded mempool transactions into local cache`);
return newTransactions;
}
public async $updateMemPoolInfo() {
this.mempoolInfo = await this.$getMempoolInfo();
}
@@ -132,7 +182,7 @@ class Mempool {
return txTimes;
}
public async $updateMempool(transactions: string[]): Promise<void> {
public async $updateMempool(transactions: string[], pollRate: number): Promise<void> {
logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes
@@ -143,7 +193,7 @@ class Mempool {
const currentMempoolSize = Object.keys(this.mempoolCache).length;
this.updateTimerProgress(timer, 'got raw mempool');
const diff = transactions.length - currentMempoolSize;
const newTransactions: MempoolTransactionExtended[] = [];
let newTransactions: MempoolTransactionExtended[] = [];
this.mempoolCacheDelta = Math.abs(diff);
@@ -162,41 +212,66 @@ class Mempool {
};
let intervalTimer = Date.now();
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
this.updateTimerProgress(timer, 'fetched new transaction');
this.mempoolCache[txid] = transaction;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
newTransactions.push(transaction);
} 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));
}
}
if (Date.now() - intervalTimer > 5_000) {
if (this.inSync) {
// Break and restart mempool loop if we spend too much time processing
// new transactions that may lead to falling behind on block height
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
break;
} else {
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);
intervalTimer = Date.now()
let loaded = false;
if (config.MEMPOOL.BACKEND === 'esplora' && currentMempoolSize < transactions.length * 0.5 && transactions.length > 20_000) {
this.inSync = false;
logger.info(`Missing ${transactions.length - currentMempoolSize} mempool transactions, attempting to reload in bulk from esplora`);
try {
newTransactions = await this.$reloadMempool(transactions.length);
if (config.REDIS.ENABLED) {
for (const tx of newTransactions) {
await redisCache.$addTransaction(tx);
}
}
loaded = true;
} catch (e) {
logger.err('failed to load mempool in bulk from esplora, falling back to fetching individual transactions');
}
}
if (!loaded) {
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
this.updateTimerProgress(timer, 'fetched new transaction');
this.mempoolCache[txid] = transaction;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
newTransactions.push(transaction);
if (config.REDIS.ENABLED) {
await redisCache.$addTransaction(transaction);
}
} 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));
}
}
if (Date.now() - intervalTimer > Math.max(pollRate * 2, 5_000)) {
if (this.inSync) {
// Break and restart mempool loop if we spend too much time processing
// new transactions that may lead to falling behind on block height
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
break;
} else {
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
if (Math.floor(progress) < 100) {
loadingIndicators.setProgress('mempool', progress);
}
intervalTimer = Date.now();
}
}
}
}
@@ -219,7 +294,7 @@ class Mempool {
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
setTimeout(() => {
this.mempoolProtection = 2;
logger.warn('Mempool clear protection resumed.');
logger.warn('Mempool clear protection ended, normal operation resumed.');
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
}
@@ -246,12 +321,6 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
if (!this.inSync && transactions.length === newMempoolSize) {
this.inSync = true;
logger.notice('The mempool is now in sync!');
loadingIndicators.setProgress('mempool', 100);
}
this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
@@ -263,6 +332,19 @@ class Mempool {
this.updateTimerProgress(timer, 'completed async mempool callback');
}
if (!this.inSync && transactions.length === newMempoolSize) {
this.inSync = true;
logger.notice('The mempool is now in sync!');
loadingIndicators.setProgress('mempool', 100);
}
// Update Redis cache
if (config.REDIS.ENABLED) {
await redisCache.$flushTransactions();
await redisCache.$removeTransactions(deletedTransactions.map(tx => tx.txid));
await rbfCache.updateCache();
}
const end = new Date().getTime();
const time = end - start;
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);

View File

@@ -11,7 +11,7 @@ import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjust
import config from '../../config';
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
import PricesRepository from '../../repositories/PricesRepository';
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import database from '../../database';
@@ -201,7 +201,7 @@ class Mining {
try {
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
const genesisTimestamp = genesisBlock.timestamp * 1000;
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
@@ -312,7 +312,7 @@ class Mining {
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
try {
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
const genesisTimestamp = genesisBlock.timestamp * 1000;
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
const lastMidnight = this.getDateMidnight(new Date());
@@ -421,8 +421,9 @@ class Mining {
}
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
let currentDifficulty = genesisBlock.difficulty;
let currentBits = genesisBlock.bits;
let totalIndexed = 0;
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
@@ -436,17 +437,18 @@ class Mining {
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
currentDifficulty = oldestConsecutiveBlock.difficulty;
currentBits = oldestConsecutiveBlock.bits;
}
let totalBlockChecked = 0;
let timer = new Date().getTime() / 1000;
for (const block of blocks) {
if (block.difficulty !== currentDifficulty) {
if (block.bits !== currentBits) {
if (indexedHeights[block.height] === true) { // Already indexed
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
currentBits = block.bits;
}
continue;
}
@@ -464,6 +466,7 @@ class Mining {
totalIndexed++;
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
currentBits = block.bits;
}
}

View File

@@ -1,15 +1,17 @@
import config from "../config";
import logger from "../logger";
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { Common } from "./common";
import redisCache from "./redis-cache";
interface RbfTransaction extends TransactionStripped {
export interface RbfTransaction extends TransactionStripped {
rbf?: boolean;
mined?: boolean;
fullRbf?: boolean;
}
interface RbfTree {
export interface RbfTree {
tx: RbfTransaction;
time: number;
interval?: number;
@@ -28,6 +30,19 @@ export interface ReplacementInfo {
newVsize: number;
}
enum CacheOp {
Remove = 0,
Add = 1,
Change = 2,
}
interface CacheEvent {
op: CacheOp;
type: 'tx' | 'tree' | 'exp';
txid: string,
value?: any,
}
class RbfCache {
private replacedBy: Map<string, string> = new Map();
private replaces: Map<string, string[]> = new Map();
@@ -36,11 +51,43 @@ class RbfCache {
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();
private cacheQueue: CacheEvent[] = [];
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
}
private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
}
private addTree(txid: string, tree: RbfTree): void {
this.rbfTrees.set(txid, tree);
this.dirtyTrees.add(txid);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tree', txid });
}
private addExpiration(txid: string, expiry: number): void {
this.expiring.set(txid, expiry);
this.cacheQueue.push({ op: CacheOp.Add, type: 'exp', txid, value: expiry });
}
private removeTx(txid: string): void {
this.txs.delete(txid);
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tx', txid });
}
private removeTree(txid: string): void {
this.rbfTrees.delete(txid);
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tree', txid });
}
private removeExpiration(txid: string): void {
this.expiring.delete(txid);
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
}
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
return;
@@ -49,7 +96,7 @@ class RbfCache {
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);
this.addTx(newTx.txid, newTxExtended);
// maintain rbf trees
let txFullRbf = false;
@@ -66,7 +113,7 @@ class RbfCache {
const treeId = this.treeMap.get(replacedTx.txid);
if (treeId) {
const tree = this.rbfTrees.get(treeId);
this.rbfTrees.delete(treeId);
this.removeTree(treeId);
if (tree) {
tree.interval = newTime - tree?.time;
replacedTrees.push(tree);
@@ -83,7 +130,7 @@ class RbfCache {
replaces: [],
});
treeFullRbf = treeFullRbf || !replacedTx.rbf;
this.txs.set(replacedTx.txid, replacedTxExtended);
this.addTx(replacedTx.txid, replacedTxExtended);
}
}
newTx.fullRbf = txFullRbf;
@@ -94,10 +141,27 @@ class RbfCache {
fullRbf: treeFullRbf,
replaces: replacedTrees
};
this.rbfTrees.set(treeId, newTree);
this.addTree(treeId, newTree);
this.updateTreeMap(treeId, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
this.dirtyTrees.add(treeId);
}
public has(txId: string): boolean {
return this.txs.has(txId);
}
public anyInSameTree(txId: string, predicate: (tx: RbfTransaction) => boolean): boolean {
const tree = this.getRbfTree(txId);
if (!tree) {
return false;
}
const txs = this.getTransactionsInTree(tree);
for (const tx of txs) {
if (predicate(tx)) {
return true;
}
}
return false;
}
public getReplacedBy(txId: string): string | undefined {
@@ -173,6 +237,7 @@ class RbfCache {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
@@ -181,7 +246,8 @@ class RbfCache {
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
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
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
@@ -202,11 +268,11 @@ class RbfCache {
const now = Date.now();
for (const txid of this.expiring.keys()) {
if ((this.expiring.get(txid) || 0) < now) {
this.expiring.delete(txid);
this.removeExpiration(txid);
this.remove(txid);
}
}
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.expiring.size} due to expire`);
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire`);
}
// remove a transaction & all previous versions from the cache
@@ -216,14 +282,14 @@ class RbfCache {
const replaces = this.replaces.get(txid);
this.replaces.delete(txid);
this.treeMap.delete(txid);
this.txs.delete(txid);
this.expiring.delete(txid);
this.removeTx(txid);
this.removeExpiration(txid);
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache
this.replacedBy.delete(tx);
// if this is the id of a tree, remove that too
if (this.treeMap.get(tx) === tx) {
this.rbfTrees.delete(tx);
this.removeTree(tx);
}
this.remove(tx);
}
@@ -255,6 +321,33 @@ class RbfCache {
}
}
public async updateCache(): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
}
// Update the Redis cache by replaying queued events
for (const e of this.cacheQueue) {
if (e.op === CacheOp.Add || e.op === CacheOp.Change) {
let value = e.value;
switch(e.type) {
case 'tx': {
value = this.txs.get(e.txid);
} break;
case 'tree': {
const tree = this.rbfTrees.get(e.txid);
value = tree ? this.exportTree(tree) : null;
} break;
}
if (value != null) {
await redisCache.$setRbfEntry(e.type, e.txid, value);
}
} else if (e.op === CacheOp.Remove) {
await redisCache.$removeRbfEntry(e.type, e.txid);
}
}
this.cacheQueue = [];
}
public dump(): any {
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
@@ -267,14 +360,14 @@ class RbfCache {
public async load({ txs, trees, expiring }): Promise<void> {
txs.forEach(txEntry => {
this.txs.set(txEntry[0], txEntry[1]);
this.txs.set(txEntry.key, txEntry.value);
});
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());
if (this.txs.has(expiringEntry.key)) {
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
}
});
this.cleanup();
@@ -360,8 +453,7 @@ class RbfCache {
};
this.treeMap.set(txid, root);
if (root === txid) {
this.rbfTrees.set(root, tree);
this.dirtyTrees.add(root);
this.addTree(root, tree);
}
return tree;
}

View File

@@ -0,0 +1,276 @@
import { createClient } from 'redis';
import memPool from './mempool';
import blocks from './blocks';
import logger from '../logger';
import config from '../config';
import { BlockExtended, BlockSummary, MempoolTransactionExtended } from '../mempool.interfaces';
import rbfCache from './rbf-cache';
import transactionUtils from './transaction-utils';
enum NetworkDB {
mainnet = 0,
testnet,
signet,
liquid,
liquidtestnet,
}
class RedisCache {
private client;
private connected = false;
private schemaVersion = 1;
private cacheQueue: MempoolTransactionExtended[] = [];
private txFlushLimit: number = 10000;
constructor() {
if (config.REDIS.ENABLED) {
const redisConfig = {
socket: {
path: config.REDIS.UNIX_SOCKET_PATH
},
database: NetworkDB[config.MEMPOOL.NETWORK],
};
this.client = createClient(redisConfig);
this.client.on('error', (e) => {
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
});
this.$ensureConnected();
}
}
private async $ensureConnected(): Promise<void> {
if (!this.connected && config.REDIS.ENABLED) {
return this.client.connect().then(async () => {
this.connected = true;
logger.info(`Redis client connected`);
const version = await this.client.get('schema_version');
if (version !== this.schemaVersion) {
// schema changed
// perform migrations or flush DB if necessary
logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
await this.client.set('schema_version', this.schemaVersion);
}
});
}
}
async $updateBlocks(blocks: BlockExtended[]) {
try {
await this.$ensureConnected();
await this.client.set('blocks', JSON.stringify(blocks));
logger.debug(`Saved latest blocks to Redis cache`);
} catch (e) {
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $updateBlockSummaries(summaries: BlockSummary[]) {
try {
await this.$ensureConnected();
await this.client.set('block-summaries', JSON.stringify(summaries));
logger.debug(`Saved latest block summaries to Redis cache`);
} catch (e) {
logger.warn(`Failed to update block summaries in Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $addTransaction(tx: MempoolTransactionExtended) {
this.cacheQueue.push(tx);
if (this.cacheQueue.length >= this.txFlushLimit) {
await this.$flushTransactions();
}
}
async $flushTransactions() {
const success = await this.$addTransactions(this.cacheQueue);
if (success) {
logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
this.cacheQueue = [];
} else {
logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
}
}
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
if (!newTransactions.length) {
return true;
}
try {
await this.$ensureConnected();
const msetData = newTransactions.map(tx => {
const minified: any = { ...tx };
delete minified.hex;
for (const vin of minified.vin) {
delete vin.inner_redeemscript_asm;
delete vin.inner_witnessscript_asm;
delete vin.scriptsig_asm;
}
for (const vout of minified.vout) {
delete vout.scriptpubkey_asm;
}
return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
});
await this.client.MSET(msetData);
return true;
} catch (e) {
logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
return false;
}
}
async $removeTransactions(transactions: string[]) {
try {
await this.$ensureConnected();
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
}
} catch (e) {
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
try {
await this.$ensureConnected();
await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
} catch (e) {
logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $removeRbfEntry(type: string, txid: string): Promise<void> {
try {
await this.$ensureConnected();
await this.client.unlink(`rbf:${type}:${txid}`);
} catch (e) {
logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $getBlocks(): Promise<BlockExtended[]> {
try {
await this.$ensureConnected();
const json = await this.client.get('blocks');
return JSON.parse(json);
} catch (e) {
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
return [];
}
}
async $getBlockSummaries(): Promise<BlockSummary[]> {
try {
await this.$ensureConnected();
const json = await this.client.get('block-summaries');
return JSON.parse(json);
} catch (e) {
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
return [];
}
}
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
const start = Date.now();
const mempool = {};
try {
await this.$ensureConnected();
const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
for (const tx of mempoolList) {
mempool[tx.key] = tx.value;
}
logger.info(`Loaded mempool from Redis cache in ${Date.now() - start} ms`);
return mempool || {};
} catch (e) {
logger.warn(`Failed to retrieve mempool from Redis cache: ${e instanceof Error ? e.message : e}`);
}
return {};
}
async $getRbfEntries(type: string): Promise<any[]> {
try {
await this.$ensureConnected();
const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
return rbfEntries;
} catch (e) {
logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: ${e instanceof Error ? e.message : e}`);
return [];
}
}
async $loadCache() {
logger.info('Restoring mempool and blocks data from Redis cache');
// Load block data
const loadedBlocks = await this.$getBlocks();
const loadedBlockSummaries = await this.$getBlockSummaries();
// Load mempool
const loadedMempool = await this.$getMempool();
this.inflateLoadedTxs(loadedMempool);
// Load rbf data
const rbfTxs = await this.$getRbfEntries('tx');
const rbfTrees = await this.$getRbfEntries('tree');
const rbfExpirations = await this.$getRbfEntries('exp');
// Set loaded data
blocks.setBlocks(loadedBlocks || []);
blocks.setBlockSummaries(loadedBlockSummaries || []);
await memPool.$setMempool(loadedMempool);
await rbfCache.load({
txs: rbfTxs,
trees: rbfTrees.map(loadedTree => loadedTree.value),
expiring: rbfExpirations,
});
}
private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) {
for (const tx of Object.values(mempool)) {
for (const vin of tx.vin) {
if (vin.scriptsig) {
vin.scriptsig_asm = transactionUtils.convertScriptSigAsm(vin.scriptsig);
transactionUtils.addInnerScriptsToVin(vin);
}
}
for (const vout of tx.vout) {
if (vout.scriptpubkey) {
vout.scriptpubkey_asm = transactionUtils.convertScriptSigAsm(vout.scriptpubkey);
}
}
}
}
private async scanKeys<T>(pattern): Promise<{ key: string, value: T }[]> {
logger.info(`loading Redis entries for ${pattern}`);
let keys: string[] = [];
const result: { key: string, value: T }[] = [];
const patternLength = pattern.length - 1;
let count = 0;
const processValues = async (keys): Promise<void> => {
const values = await this.client.MGET(keys);
for (let i = 0; i < values.length; i++) {
if (values[i]) {
result.push({ key: keys[i].slice(patternLength), value: JSON.parse(values[i]) });
count++;
}
}
logger.info(`loaded ${count} entries from Redis cache`);
};
for await (const key of this.client.scanIterator({
MATCH: pattern,
COUNT: 100
})) {
keys.push(key);
if (keys.length >= 10000) {
await processValues(keys);
keys = [];
}
}
if (keys.length) {
await processValues(keys);
}
return result;
}
}
export default new RedisCache();

View File

@@ -3,6 +3,7 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface';
import { Common } from './common';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib';
import logger from '../logger';
class TransactionUtils {
constructor() { }
@@ -22,6 +23,23 @@ class TransactionUtils {
};
}
// Wrapper for $getTransactionExtended with an automatic retry direct to Core if the first API request fails.
// Propagates any error from the retry request.
public async $getTransactionExtendedRetry(txid: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
try {
const result = await this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, addMempoolData);
if (result) {
return result;
} else {
logger.err(`Cannot fetch tx ${txid}. Reason: backend returned null data`);
}
} catch (e) {
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
}
// retry direct from Core if first request failed
return this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, true, addMempoolData);
}
/**
* @param txId
* @param addPrevouts
@@ -31,10 +49,17 @@ class TransactionUtils {
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
let transaction: IEsploraApi.Transaction;
if (forceCore === true) {
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
transaction = await bitcoinCoreApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
} else {
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
}
if (Common.isLiquid()) {
if (!isFinite(Number(transaction.fee))) {
transaction.fee = Object.values(transaction.fee || {}).reduce((total, output) => total + output, 0);
}
}
if (addMempoolData || !transaction?.status?.confirmed) {
return this.extendMempoolTransaction(transaction);
} else {
@@ -46,14 +71,13 @@ class TransactionUtils {
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
}
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
// @ts-ignore
if (transaction.vsize) {
// @ts-ignore
return transaction;
}
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
(transaction.fee || 0) / (transaction.weight / 4));
const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4);
const transactionExtended: TransactionExtended = Object.assign({
vsize: Math.round(transaction.weight / 4),
feePerVsize: feePerVbytes,
@@ -68,13 +92,11 @@ class TransactionUtils {
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
const vsize = Math.ceil(transaction.weight / 4);
const fractionalVsize = (transaction.weight / 4);
const sigops = this.countSigops(transaction);
const sigops = !Common.isLiquid() ? this.countSigops(transaction) : 0;
// 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 feePerVbytes = (transaction.fee || 0) / fractionalVsize;
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
order: this.txidToOrdering(transaction.txid),
vsize: Math.round(transaction.weight / 4),
@@ -166,6 +188,122 @@ class TransactionUtils {
16
);
}
public addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
if (!vin.prevout) {
return;
}
if (vin.prevout.scriptpubkey_type === 'p2sh') {
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
if (vin.witness && vin.witness.length > 2) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
}
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
const witnessScript = this.witnessToP2TRScript(vin.witness);
if (witnessScript !== null) {
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
}
}
public convertScriptSigAsm(hex: string): string {
const buf = Buffer.from(hex, 'hex');
const b: string[] = [];
let i = 0;
while (i < buf.length) {
const op = buf[i];
if (op >= 0x01 && op <= 0x4e) {
i++;
let push: number;
if (op === 0x4c) {
push = buf.readUInt8(i);
b.push('OP_PUSHDATA1');
i += 1;
} else if (op === 0x4d) {
push = buf.readUInt16LE(i);
b.push('OP_PUSHDATA2');
i += 2;
} else if (op === 0x4e) {
push = buf.readUInt32LE(i);
b.push('OP_PUSHDATA4');
i += 4;
} else {
push = op;
b.push('OP_PUSHBYTES_' + push);
}
const data = buf.slice(i, i + push);
if (data.length !== push) {
break;
}
b.push(data.toString('hex'));
i += data.length;
} else {
if (op === 0x00) {
b.push('OP_0');
} else if (op === 0x4f) {
b.push('OP_PUSHNUM_NEG1');
} else if (op === 0xb1) {
b.push('OP_CLTV');
} else if (op === 0xb2) {
b.push('OP_CSV');
} else if (op === 0xba) {
b.push('OP_CHECKSIGADD');
} else {
const opcode = bitcoinjs.script.toASM([ op ]);
if (opcode && op < 0xfd) {
if (/^OP_(\d+)$/.test(opcode)) {
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
} else {
b.push(opcode);
}
} else {
b.push('OP_RETURN_' + op);
}
}
i += 1;
}
}
return b.join(' ');
}
/**
* 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.
*/
public 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 new TransactionUtils();

View File

@@ -183,15 +183,25 @@ class WebsocketHandler {
}
if (parsedMessage && parsedMessage['track-address']) {
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/
.test(parsedMessage['track-address'])) {
let matchedAddress = parsedMessage['track-address'];
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
matchedAddress = matchedAddress.toLowerCase();
}
client['track-address'] = matchedAddress;
if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
client['track-address'] = null;
client['track-scriptpubkey'] = '41' + matchedAddress + 'ac';
} else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
client['track-address'] = null;
client['track-scriptpubkey'] = '21' + matchedAddress + 'ac';
} else {
client['track-address'] = matchedAddress;
client['track-scriptpubkey'] = null;
}
} else {
client['track-address'] = null;
client['track-scriptpubkey'] = null;
}
}
@@ -546,6 +556,44 @@ class WebsocketHandler {
}
}
if (client['track-scriptpubkey']) {
const foundTransactions: TransactionExtended[] = [];
for (const tx of newTransactions) {
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']);
if (someVin) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
} else {
foundTransactions.push(tx);
}
return;
}
const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']);
if (someVout) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
} else {
foundTransactions.push(tx);
}
}
}
if (foundTransactions.length) {
response['address-transactions'] = JSON.stringify(foundTransactions);
}
}
if (client['track-asset']) {
const foundTransactions: TransactionExtended[] = [];
@@ -604,7 +652,7 @@ class WebsocketHandler {
}
}
if (client['track-mempool-block'] >= 0) {
if (client['track-mempool-block'] >= 0 && memPool.isInSync()) {
const index = client['track-mempool-block'];
if (mBlockDeltas[index]) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
@@ -644,7 +692,7 @@ class WebsocketHandler {
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT) {
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
let projectedBlocks;
let auditMempool = _memPool;
// template calculation functions have mempool side effects, so calculate audits using
@@ -665,7 +713,7 @@ class WebsocketHandler {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
}
if (Common.indexingEnabled() && memPool.isInSync()) {
if (Common.indexingEnabled()) {
const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
@@ -821,6 +869,33 @@ class WebsocketHandler {
}
}
if (client['track-scriptpubkey']) {
const foundTransactions: TransactionExtended[] = [];
transactions.forEach((tx) => {
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) {
foundTransactions.push(tx);
return;
}
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) {
foundTransactions.push(tx);
}
});
if (foundTransactions.length) {
foundTransactions.forEach((tx) => {
tx.status = {
confirmed: true,
block_height: block.height,
block_hash: block.id,
block_time: block.timestamp,
};
});
response['block-transactions'] = JSON.stringify(foundTransactions);
}
}
if (client['track-asset']) {
const foundTransactions: TransactionExtended[] = [];
@@ -858,7 +933,7 @@ class WebsocketHandler {
}
}
if (client['track-mempool-block'] >= 0) {
if (client['track-mempool-block'] >= 0 && memPool.isInSync()) {
const index = client['track-mempool-block'];
if (mBlockDeltas && mBlockDeltas[index]) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {

View File

@@ -12,6 +12,7 @@ interface IConfig {
API_URL_PREFIX: string;
POLL_RATE_MS: number;
CACHE_DIR: string;
CACHE_ENABLED: boolean;
CLEAR_PROTECTION_MINUTES: number;
RECOMMENDED_FEE_PERCENTILE: number;
BLOCK_WEIGHT_UNITS: number;
@@ -137,7 +138,11 @@ interface IConfig {
AUDIT: boolean;
AUDIT_START_HEIGHT: number;
SERVERS: string[];
}
},
REDIS: {
ENABLED: boolean;
UNIX_SOCKET_PATH: string;
},
}
const defaults: IConfig = {
@@ -150,6 +155,7 @@ const defaults: IConfig = {
'API_URL_PREFIX': '/api/v1/',
'POLL_RATE_MS': 2000,
'CACHE_DIR': './cache',
'CACHE_ENABLED': true,
'CLEAR_PROTECTION_MINUTES': 20,
'RECOMMENDED_FEE_PERCENTILE': 50,
'BLOCK_WEIGHT_UNITS': 4000000,
@@ -275,7 +281,11 @@ const defaults: IConfig = {
'AUDIT': false,
'AUDIT_START_HEIGHT': 774000,
'SERVERS': [],
}
},
'REDIS': {
'ENABLED': false,
'UNIX_SOCKET_PATH': '',
},
};
class Config implements IConfig {
@@ -296,6 +306,7 @@ class Config implements IConfig {
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
MAXMIND: IConfig['MAXMIND'];
REPLICATION: IConfig['REPLICATION'];
REDIS: IConfig['REDIS'];
constructor() {
const configs = this.merge(configFromFile, defaults);
@@ -316,6 +327,7 @@ class Config implements IConfig {
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
this.MAXMIND = configs.MAXMIND;
this.REPLICATION = configs.REPLICATION;
this.REDIS = configs.REDIS;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -41,6 +41,7 @@ import chainTips from './api/chain-tips';
import { AxiosError } from 'axios';
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
import redisCache from './api/redis-cache';
class Server {
private wss: WebSocket.Server | undefined;
@@ -122,7 +123,11 @@ class Server {
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$();
if (config.MEMPOOL.ENABLED) {
await diskCache.$loadMempoolCache();
if (config.MEMPOOL.CACHE_ENABLED) {
await diskCache.$loadMempoolCache();
} else if (config.REDIS.ENABLED) {
await redisCache.$loadCache();
}
}
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
@@ -183,14 +188,15 @@ class Server {
}
const newMempool = await bitcoinApi.$getRawMempool();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool);
await memPool.$updateMempool(newMempool, pollRate);
}
indexer.$run();
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
const elapsed = Date.now() - start;
const remainingTime = Math.max(0, config.MEMPOOL.POLL_RATE_MS - elapsed)
const remainingTime = Math.max(0, pollRate - elapsed);
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 0 : remainingTime);
this.backendRetryCount = 0;
} catch (e: any) {

View File

@@ -1,3 +1,4 @@
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import { BlockExtended, BlockExtension, BlockPrice, EffectiveFeeStats } from '../mempool.interfaces';
import DB from '../database';
import logger from '../logger';
@@ -12,6 +13,7 @@ import config from '../config';
import chainTips from '../api/chain-tips';
import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils';
interface DatabaseBlock {
id: string;
@@ -539,7 +541,7 @@ class BlocksRepository {
*/
public async $getBlocksDifficulty(): Promise<object[]> {
try {
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty FROM blocks`);
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`);
return rows;
} catch (e) {
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));
@@ -848,7 +850,7 @@ class BlocksRepository {
*/
public async $getOldestConsecutiveBlock(): Promise<any> {
try {
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty FROM blocks ORDER BY height DESC`);
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, bits FROM blocks ORDER BY height DESC`);
for (let i = 0; i < rows.length - 1; ++i) {
if (rows[i].height - rows[i + 1].height > 1) {
return rows[i];
@@ -1036,8 +1038,17 @@ class BlocksRepository {
{
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
if (extras.feePercentiles === null) {
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
const summary = blocks.summarizeBlock(block);
let summary;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
} else {
// Call Core RPC
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
summary = blocks.summarizeBlock(block);
}
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions);
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
}

3
contributors/Czino.txt Normal file
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 July 29, 2023.
Signed: Czino

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

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 July 21, 2023.
Signed: devinbileck

5
contributors/fiatjaf.txt Normal file
View File

@@ -0,0 +1,5 @@
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.
I also regret having ever contributed to this repository since they keep asking me to sign this legalese timewaste things.
And finally I don't care about licenses and won't sue anyone over intellectual property, which is a fake statist construct invented by evil lobby lawyers.
Signed: fiatjaf

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 April 7, 203.
Signed: learntheropes

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 April 8, 2023.
Signed: nothing0012

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 July 20, 2023.
Signed: pedromvpg

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 July 29, 2023.
Signed: rishkwal

View File

@@ -7,9 +7,10 @@ WORKDIR /build
COPY . .
RUN apt-get update
RUN apt-get install -y build-essential python3 pkg-config curl
RUN apt-get install -y build-essential python3 pkg-config curl ca-certificates
# Install Rust via rustup
RUN CPU_ARCH=$(uname -m); if [ "$CPU_ARCH" = "armv7l" ]; then c_rehash; fi
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
ENV PATH="/root/.cargo/bin:$PATH"

View File

@@ -8,6 +8,7 @@
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CACHE_ENABLED": __MEMPOOL_CACHE_ENABLED__,
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_CLEAR_PROTECTION_MINUTES__,
"RECOMMENDED_FEE_PERCENTILE": __MEMPOOL_RECOMMENDED_FEE_PERCENTILE__,
"BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__,
@@ -133,5 +134,9 @@
"AUDIT": __REPLICATION_AUDIT__,
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
"SERVERS": __REPLICATION_SERVERS__
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
}
}

View File

@@ -9,6 +9,7 @@ __MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
__MEMPOOL_CACHE_ENABLED__=${MEMPOOL_CACHE_ENABLED:=true}
__MEMPOOL_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__=${MEMPOOL_RECOMMENDED_FEE_PERCENTILE:=50}
__MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
@@ -136,6 +137,9 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@@ -147,6 +151,7 @@ sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g"
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CACHE_ENABLED__!${__MEMPOOL_CACHE_ENABLED__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
@@ -165,7 +170,7 @@ sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-co
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_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_GBT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!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_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
@@ -262,4 +267,8 @@ sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
# REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
node /backend/package/index.js

View File

@@ -18,7 +18,7 @@ fi
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
__LIQUID_ENABLED__=${LIQUID_EANBLED:=false}
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}
@@ -39,7 +39,6 @@ __AUDIT__=${AUDIT:=false}
__MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_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}
__FULL_RBF_ENABLED__=${FULL_RBF_ENABLED:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
# Export as environment variables to be used by envsubst
@@ -66,7 +65,6 @@ export __AUDIT__
export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
export __FULL_RBF_ENABLED__
export __HISTORICAL_PRICE__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)

View File

@@ -2,11 +2,15 @@
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
last 2 Chrome versions
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

View File

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

15031
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -61,60 +61,60 @@
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^14.2.10",
"@angular/animations": "^14.2.12",
"@angular/cli": "^14.2.10",
"@angular/common": "^14.2.12",
"@angular/compiler": "^14.2.12",
"@angular/core": "^14.2.12",
"@angular/forms": "^14.2.12",
"@angular/localize": "^14.2.12",
"@angular/platform-browser": "^14.2.12",
"@angular/platform-browser-dynamic": "^14.2.12",
"@angular/platform-server": "^14.2.12",
"@angular/router": "^14.2.12",
"@fortawesome/angular-fontawesome": "~0.11.1",
"@fortawesome/fontawesome-common-types": "~6.2.1",
"@fortawesome/fontawesome-svg-core": "~6.2.1",
"@fortawesome/free-solid-svg-icons": "~6.2.1",
"@angular-devkit/build-angular": "^16.1.4",
"@angular/animations": "^16.1.5",
"@angular/cli": "^16.1.4",
"@angular/common": "^16.1.5",
"@angular/compiler": "^16.1.5",
"@angular/core": "^16.1.5",
"@angular/forms": "^16.1.5",
"@angular/localize": "^16.1.5",
"@angular/platform-browser": "^16.1.5",
"@angular/platform-browser-dynamic": "^16.1.5",
"@angular/platform-server": "^16.1.5",
"@angular/router": "^16.1.5",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-common-types": "~6.4.0",
"@fortawesome/fontawesome-svg-core": "~6.4.0",
"@fortawesome/free-solid-svg-icons": "~6.4.0",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^13.1.1",
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
"@types/qrcode": "~1.5.0",
"bootstrap": "~4.6.1",
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.4.1",
"echarts": "~5.4.3",
"echarts-gl": "^2.0.9",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~14.0.0",
"ngx-infinite-scroll": "^14.0.1",
"ngx-echarts": "~16.0.0",
"ngx-infinite-scroll": "^16.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.0",
"tinyify": "^3.1.0",
"rxjs": "~7.8.1",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.4.1",
"zone.js": "~0.12.0"
"tslib": "~2.6.0",
"zone.js": "~0.13.1"
},
"devDependencies": {
"@angular/compiler-cli": "^14.2.12",
"@angular/language-service": "^14.2.12",
"@angular/compiler-cli": "^16.1.5",
"@angular/language-service": "^16.1.5",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.31.0",
"http-proxy-middleware": "~2.0.6",
"prettier": "^2.8.2",
"prettier": "^3.0.0",
"ts-node": "~10.9.1",
"typescript": "~4.6.4"
"typescript": "~4.9.3"
},
"optionalDependencies": {
"@cypress/schematic": "^2.4.0",
"cypress": "^12.7.0",
"cypress-fail-on-console-error": "~4.0.2",
"@cypress/schematic": "^2.5.0",
"cypress": "^12.17.1",
"cypress-fail-on-console-error": "~4.0.3",
"cypress-wait-until": "^1.7.2",
"mock-socket": "~9.1.5",
"start-server-and-test": "~1.14.0"
"mock-socket": "~9.2.1",
"start-server-and-test": "~2.0.0"
},
"scarfSettings": {
"enabled": false

View File

@@ -1,4 +1,4 @@
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@@ -48,8 +48,7 @@ const providers = [
AppComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
BrowserTransferStateModule,
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,

View File

@@ -281,3 +281,15 @@ export function isFeatureActive(network: string, height: number, feature: 'rbf'
return false;
}
}
export async function calcScriptHash$(script: string): Promise<string> {
if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
throw new Error('script is not a valid hex string');
}
const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16)));
const hashBuffer = await crypto.subtle.digest('SHA-256', buf);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
}

View File

@@ -411,7 +411,7 @@
Trademark Notice<br>
</div>
<p>
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.
The Mempool Open Source Project&reg;, 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>
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;.

View File

@@ -64,13 +64,15 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.address = null;
this.addressInfo = null;
this.addressString = params.get('id') || '';
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
return this.electrsApiService.getAddress$(this.addressString)
.pipe(
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)
: this.electrsApiService.getAddress$(this.addressString)
).pipe(
catchError((err) => {
this.isLoadingAddress = false;
this.error = err;

View File

@@ -81,6 +81,7 @@ h1 {
top: 11px;
}
@media (min-width: 768px) {
max-width: calc(100% - 180px);
top: 17px;
}
}

View File

@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, Transaction } from '../../interfaces/electrs.interface';
import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
@@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.addressInfo = null;
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
@@ -83,8 +83,11 @@ export class AddressComponent implements OnInit, OnDestroy {
.pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0))
)
.pipe(
switchMap(() => this.electrsApiService.getAddress$(this.addressString)
.pipe(
switchMap(() => (
this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)
: this.electrsApiService.getAddress$(this.addressString)
).pipe(
catchError((err) => {
this.isLoadingAddress = false;
this.error = err;
@@ -114,7 +117,9 @@ export class AddressComponent implements OnInit, OnDestroy {
this.updateChainStats();
this.isLoadingAddress = false;
this.isLoadingTransactions = true;
return this.electrsApiService.getAddressTransactions$(address.address);
return address.is_pubkey
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
: this.electrsApiService.getAddressTransactions$(address.address);
}),
switchMap((transactions) => {
this.tempTransactions = transactions;
@@ -161,31 +166,8 @@ export class AddressComponent implements OnInit, OnDestroy {
});
this.stateService.mempoolTransactions$
.subscribe((transaction) => {
if (this.transactions.some((t) => t.txid === transaction.txid)) {
return;
}
this.transactions.unshift(transaction);
this.transactions = this.transactions.slice();
this.txCount++;
if (transaction.vout.some((vout) => vout.scriptpubkey_address === this.address.address)) {
this.audioService.playSound('cha-ching');
} else {
this.audioService.playSound('chime');
}
transaction.vin.forEach((vin) => {
if (vin.prevout.scriptpubkey_address === this.address.address) {
this.sent += vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout.scriptpubkey_address === this.address.address) {
this.received += vout.value;
}
});
.subscribe(tx => {
this.addTransaction(tx);
});
this.stateService.blockTransactions$
@@ -195,12 +177,47 @@ export class AddressComponent implements OnInit, OnDestroy {
tx.status = transaction.status;
this.transactions = this.transactions.slice();
this.audioService.playSound('magic');
} else {
if (this.addTransaction(transaction, false)) {
this.audioService.playSound('magic');
}
}
this.totalConfirmedTxCount++;
this.loadedConfirmedTxCount++;
});
}
addTransaction(transaction: Transaction, playSound: boolean = true): boolean {
if (this.transactions.some((t) => t.txid === transaction.txid)) {
return false;
}
this.transactions.unshift(transaction);
this.transactions = this.transactions.slice();
this.txCount++;
if (playSound) {
if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
this.audioService.playSound('cha-ching');
} else {
this.audioService.playSound('chime');
}
}
transaction.vin.forEach((vin) => {
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
this.sent += vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout?.scriptpubkey_address === this.address.address) {
this.received += vout.value;
}
});
return true;
}
loadMore() {
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
return;

View File

@@ -40,8 +40,8 @@
</a>
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
<app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images>
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" name="bisq" width="20" height="20" viewBox="0 0 80 80"></app-svg-images>
</button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '/')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>

View File

@@ -147,3 +147,18 @@ nav {
.navbar-brand {
margin-right: 5px;
}
.current-network-svg {
width: 20px;
height: 20px;
margin-right: 5px;
}
:host-context(.rtl-layout) {
.current-network-svg {
width: 20px;
height: 20px;
margin-left: 5px;
margin-right: 0px;
}
}

View File

@@ -25,7 +25,8 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 250px);
height: calc(100vh - 225px);
min-height: 400px;
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -25,7 +25,8 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 250px);
height: calc(100vh - 225px);
min-height: 400px;
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -25,7 +25,8 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 250px);
height: calc(100vh - 225px);
min-height: 400px;
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -38,7 +38,7 @@ export default class TxView implements TransactionStripped {
value: number;
feerate: number;
rate?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
context?: 'projected' | 'actual';
scene?: BlockScene;
@@ -207,7 +207,7 @@ export default class TxView implements TransactionStripped {
return auditColors.censored;
case 'missing':
case 'sigop':
case 'fullrbf':
case 'rbf':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'fresh':
case 'freshcpfp':

View File

@@ -53,7 +53,7 @@
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
</ng-container>
</tr>
</tbody>

View File

@@ -25,7 +25,8 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 250px);
height: calc(100vh - 225px);
min-height: 400px;
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -25,7 +25,8 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 250px);
height: calc(100vh - 225px);
min-height: 400px;
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -144,10 +144,12 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const block of blocks) {
if (block.id === this.blockHash) {
this.block = block;
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
if (block?.extras?.reward != undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
if (block.extras) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
if (block?.extras?.reward != undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
}
}
} else if (block.height === this.block?.height) {
this.block.stale = true;
@@ -246,8 +248,10 @@ export class BlockComponent implements OnInit, OnDestroy {
}
this.updateAuditAvailableFromBlockHeight(block.height);
this.block = block;
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
if (block.extras) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
}
this.blockHeight = block.height;
this.lastBlockHeight = this.blockHeight;
this.nextBlockHeight = block.height + 1;
@@ -335,7 +339,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isSelected = {};
const isFresh = {};
const isSigop = {};
const isFullRbf = {};
const isRbf = {};
this.numMissing = 0;
this.numUnexpected = 0;
@@ -359,7 +363,7 @@ export class BlockComponent implements OnInit, OnDestroy {
isSigop[txid] = true;
}
for (const txid of blockAudit.fullrbfTxs || []) {
isFullRbf[txid] = true;
isRbf[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
@@ -377,8 +381,8 @@ export class BlockComponent implements OnInit, OnDestroy {
}
} else if (isSigop[tx.txid]) {
tx.status = 'sigop';
} else if (isFullRbf[tx.txid]) {
tx.status = 'fullrbf';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else {
tx.status = 'missing';
}
@@ -394,8 +398,8 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else if (isFullRbf[tx.txid]) {
tx.status = 'fullrbf';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;

View File

@@ -113,8 +113,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
const animate = this.chainTip != null && latestHeight > this.chainTip;
for (const block of blocks) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
if (block?.extras) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
}
}
this.blocks = blocks;
@@ -251,7 +253,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (height >= 0) {
this.cacheService.loadBlock(height);
block = this.cacheService.getCachedBlock(height) || null;
if (block) {
if (block?.extras) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
}
@@ -293,8 +295,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
onBlockLoaded(block: BlockExtended) {
const blockIndex = this.height - block.height;
if (blockIndex >= 0 && blockIndex < this.blocks.length) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
if (block?.extras) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
}
this.blocks[blockIndex] = block;
this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex);
}

View File

@@ -82,9 +82,7 @@ export class BlockchainComponent implements OnInit, OnDestroy {
}
this.mempoolOffset = Math.max(0, width - this.dividerOffset);
this.cd.markForCheck();
setTimeout(() => {
this.mempoolOffsetChange.emit(this.mempoolOffset);
}, 0);
this.mempoolOffsetChange.emit(this.mempoolOffset);
}
@HostListener('window:resize', ['$event'])

View File

@@ -68,7 +68,7 @@ export class BlocksList implements OnInit {
for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template
block.extras.pool.logo = `/resources/mining-pools/` +
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
block.extras.pool.slug + '.svg';
}
}
if (this.widget) {
@@ -84,10 +84,10 @@ export class BlocksList implements OnInit {
.pipe(
switchMap((blocks) => {
if (blocks[0].height <= this.lastBlockHeight) {
return [null]; // Return an empty stream so the last pipe is not executed
return of([]); // Return an empty stream so the last pipe is not executed
}
this.lastBlockHeight = blocks[0].height;
return blocks;
return of(blocks);
})
)
])
@@ -102,7 +102,7 @@ export class BlocksList implements OnInit {
if (this.stateService.env.MINING_DASHBOARD) {
// @ts-ignore: Need to add an extra field for the template
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
blocks[1][0].extras.pool.slug + '.svg';
}
acc.unshift(blocks[1][0]);
acc = acc.slice(0, this.widget ? 6 : 15);

View File

@@ -9,6 +9,7 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow: hidden;
--chain-height: 60px;
--clock-width: 300px;

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../..//services/state.service';
@@ -61,6 +61,7 @@ export class DifficultyComponent implements OnInit {
constructor(
public stateService: StateService,
private cd: ChangeDetectorRef,
@Inject(LOCALE_ID) private locale: string,
) { }
@@ -189,9 +190,15 @@ export class DifficultyComponent implements OnInit {
return shapes;
}
@HostListener('pointerdown', ['$event'])
onPointerDown(event) {
this.onPointerMove(event);
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
this.cd.markForCheck();
}
onHover(event, rect): void {

View File

@@ -25,7 +25,8 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 250px);
height: calc(100vh - 225px);
min-height: 400px;
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -25,7 +25,8 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 250px);
height: calc(100vh - 225px);
min-height: 400px;
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -45,8 +45,8 @@
</a>
<div ngbDropdown (window:resize)="onResize()" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
<app-svg-images [name]="network.val === '' ? 'liquid' : network.val" width="22" height="22" viewBox="0 0 125 125" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'liquid' : network.val" width="20" height="20" viewBox="0 0 125 125"></app-svg-images>
</button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>

View File

@@ -136,4 +136,19 @@ nav {
}
.navbar-dark .navbar-nav .nav-link {
color: #f1f1f1;
}
.current-network-svg {
width: 20px;
height: 20px;
margin-right: 5px;
}
:host-context(.rtl-layout) {
.current-network-svg {
width: 20px;
height: 20px;
margin-left: 5px;
margin-right: 0px;
}
}

View File

@@ -18,8 +18,8 @@
</a>
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
<app-svg-images [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images>
</button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>

View File

@@ -64,7 +64,9 @@ li.nav-item {
.navbar-collapse {
flex-basis: auto;
@media (min-width: 564px) {
flex-basis: auto;
}
justify-content: flex-end;
}
@@ -193,3 +195,18 @@ nav {
font-size: 7px;
}
}
.current-network-svg {
width: 20px;
height: 20px;
margin-right: 5px;
}
:host-context(.rtl-layout) {
.current-network-svg {
width: 20px;
height: 20px;
margin-left: 5px;
margin-right: 0px;
}
}

View File

@@ -31,6 +31,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
@Input() allBlocks: boolean = false;
mempoolWidth: number = 0;
@Output() widthChange: EventEmitter<number> = new EventEmitter();
specialBlocks = specialBlocks;
@@ -49,6 +50,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
blockSubscription: Subscription;
networkSubscription: Subscription;
chainTipSubscription: Subscription;
keySubscription: Subscription;
isTabHiddenSubscription: Subscription;
network = '';
now = new Date().getTime();
timeOffset = 0;
@@ -115,8 +118,15 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.calculateTransactionPosition();
});
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
this.isTabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
this.loadingBlocks$ = combineLatest([
this.stateService.isLoadingWebSocket$,
this.stateService.isLoadingMempool$
]).pipe(
switchMap(([loadingBlocks, loadingMempool]) => {
return of(loadingBlocks || loadingMempool);
})
);
this.mempoolBlocks$ = merge(
of(true),
@@ -155,7 +165,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
}),
tap(() => {
this.cd.markForCheck();
this.widthChange.emit(this.containerOffset + this.mempoolBlocks.length * this.blockOffset);
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
if (this.mempoolWidth !== width) {
this.mempoolWidth = width;
this.widthChange.emit(this.mempoolWidth);
}
})
);
@@ -212,7 +226,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.networkSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
this.stateService.keyNavigation$.subscribe((event) => {
this.keySubscription = this.stateService.keyNavigation$.subscribe((event) => {
if (this.markIndex === undefined) {
return;
}
@@ -223,13 +237,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (this.mempoolBlocks[this.markIndex - 1]) {
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
} else {
this.stateService.blocks$
.pipe(map((blocks) => blocks[0]))
.subscribe((block) => {
if (this.stateService.latestBlockHeight === block.height) {
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
}
});
const blocks = this.stateService.blocksSubject$.getValue();
for (const block of (blocks || [])) {
if (this.stateService.latestBlockHeight === block.height) {
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
}
}
}
} else if (event.key === nextKey) {
if (this.mempoolBlocks[this.markIndex + 1]) {
@@ -253,6 +266,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.networkSubscription.unsubscribe();
this.timeLtrSubscription.unsubscribe();
this.chainTipSubscription.unsubscribe();
this.keySubscription.unsubscribe();
this.isTabHiddenSubscription.unsubscribe();
clearTimeout(this.resetTransitionTimeout);
}

View File

@@ -51,7 +51,7 @@
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<app-blocks-list [attr.data-cy]="'latest-blocks'" [widget]=true></app-blocks-list>
</div>
@@ -65,7 +65,7 @@
<a class="title-link" href="" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.adjustments">Adjustments</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<app-difficulty-adjustments-table [attr.data-cy]="'difficulty-adjustments-table'"></app-difficulty-adjustments-table>
</div>

View File

@@ -1,6 +1,8 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { EventType, NavigationStart, Router } from '@angular/router';
@Component({
selector: 'app-mining-dashboard',
@@ -8,10 +10,12 @@ import { WebsocketService } from '../../services/websocket.service';
styleUrls: ['./mining-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MiningDashboardComponent implements OnInit {
export class MiningDashboardComponent implements OnInit, AfterViewInit {
constructor(
private seoService: SeoService,
private websocketService: WebsocketService,
private stateService: StateService,
private router: Router
) {
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
}
@@ -19,4 +23,15 @@ export class MiningDashboardComponent implements OnInit {
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
}
ngAfterViewInit(): void {
this.stateService.focusSearchInputDesktop();
this.router.events.subscribe((e: NavigationStart) => {
if (e.type === EventType.NavigationStart) {
if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
this.stateService.focusSearchInputDesktop();
}
}
});
}
}

View File

@@ -139,6 +139,8 @@
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
miningStats.miningUnits.hashrateUnit }}</b></td>
<td class=""><b>{{ miningStats.blockCount }}</b></td>
<td *ngIf="auditAvailable"></td>
<td *ngIf="auditAvailable"></td>
<td class="d-none d-md-table-cell"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
}}%)</b></td>
</tr>

View File

@@ -89,7 +89,7 @@ export class PoolPreviewComponent implements OnInit {
this.openGraphService.waitOver('pool-stats-' + this.slug);
const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg';
if (logoSrc === this.lastImgSrc) {
this.openGraphService.waitOver('pool-img-' + this.slug);
}

View File

@@ -92,9 +92,9 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.reward">Reward</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
</tr>
</thead>
<tbody>
@@ -117,9 +117,9 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.reward">Reward</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="latest-blocks.avg_health" *ngIf="auditAvailable">Avg Health</th>
</tr>
</thead>
<tbody>
@@ -143,9 +143,9 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr>
</thead>
<tbody>
@@ -165,9 +165,9 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr>
</thead>
<tbody>
@@ -382,9 +382,9 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
</tr>
</thead>
<tbody>
@@ -407,9 +407,9 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
<th scope="col" class="data-title clip text-center" style="width: 33%" i18n="mining.luck" *ngIf="auditAvailable">Avg Health</th>
</tr>
</thead>
<tbody>
@@ -433,9 +433,9 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr>
</thead>
<tbody>
@@ -458,9 +458,9 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
</tr>
</thead>
<tbody>

View File

@@ -188,11 +188,19 @@ div.scrollable {
}
}
.block-count-title {
.data-title {
color: #4a68b9;
font-size: 14px;
}
.clip {
@media (max-width: 576px) {
max-width: 85px;
overflow: hidden;
text-overflow: ellipsis;
}
}
.table-data tr {
background-color: transparent;
}

View File

@@ -79,7 +79,7 @@ export class PoolComponent implements OnInit {
poolStats.pool.regexes = regexes.slice(0, -3);
return Object.assign({
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
logo: `/resources/mining-pools/` + poolStats.pool.slug + '.svg'
}, poolStats);
})
);

View File

@@ -43,7 +43,7 @@
<h4>TRUST YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project&trade; on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project&reg; on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
<br>

View File

@@ -2,7 +2,7 @@
<h1 class="float-left" i18n="page.rbf-replacements">RBF Replacements</h1>
<div *ngIf="isLoading" class="spinner-border ml-3" role="status"></div>
<div class="mode-toggle float-right" *ngIf="fullRbfEnabled">
<div class="mode-toggle float-right">
<form class="formRadioGroup">
<div class="btn-group btn-group-toggle" name="radioBasic">
<label class="btn btn-primary btn-sm" [class.active]="!fullRbf">
@@ -22,12 +22,12 @@
<div *ngFor="let tree of trees" class="tree">
<p class="info">
<span class="type">
<span *ngIf="isMined(tree)" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
<span *ngIf="isFullRbf(tree)" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
<span *ngIf="tree.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
<span *ngIf="tree.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
</span>
<app-time kind="since" [time]="tree.time"></app-time>
</p>
<div class="timeline-wrapper" [class.mined]="isMined(tree)">
<div class="timeline-wrapper" [class.mined]="tree.mined">
<app-rbf-timeline [replacements]="tree"></app-rbf-timeline>
</div>
</div>
@@ -36,11 +36,6 @@
<p i18n="rbf.no-replacements-yet">there are no replacements in the mempool yet!</p>
</div>
</ng-container>
<!-- <ngb-pagination class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="blocksCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination> -->
</div>
</div>

View File

@@ -17,7 +17,6 @@ export class RbfList implements OnInit, OnDestroy {
rbfTrees$: Observable<RbfTree[]>;
nextRbfSubject = new BehaviorSubject(null);
urlFragmentSubscription: Subscription;
fullRbfEnabled: boolean;
fullRbf: boolean;
isLoading = true;
@@ -27,9 +26,7 @@ export class RbfList implements OnInit, OnDestroy {
private apiService: ApiService,
public stateService: StateService,
private websocketService: WebsocketService,
) {
this.fullRbfEnabled = stateService.env.FULL_RBF_ENABLED;
}
) { }
ngOnInit(): void {
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
@@ -56,25 +53,6 @@ export class RbfList implements OnInit, OnDestroy {
);
}
toggleFullRbf(event) {
this.router.navigate([], {
relativeTo: this.route,
fragment: this.fullRbf ? null : 'fullrbf'
});
}
isFullRbf(tree: RbfTree): boolean {
return tree.fullRbf;
}
isMined(tree: RbfTree): boolean {
return tree.mined;
}
// pageChange(page: number) {
// this.fromTreeSubject.next(this.lastTreeId);
// }
ngOnDestroy(): void {
this.websocketService.stopTrackRbf();
}

View File

@@ -1,7 +1,7 @@
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
<div class="d-flex">
<div class="search-box-container mr-2">
<input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
</div>
<div>

View File

@@ -18,9 +18,10 @@
form {
margin-top: 5px;
@media (min-width: 576px) {
@media (min-width: 564px) {
margin-top: 0px;
margin-left: 8px;
margin-left: 5px;
margin-right: -5px;
}
@media (min-width: 992px) {
width: 100%;

View File

@@ -1,6 +1,6 @@
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { EventType, NavigationStart, Router } from '@angular/router';
import { AssetsService } from '../../services/assets.service';
import { StateService } from '../../services/state.service';
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
@@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
}
}
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/;
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
regexBlockheight = /^[0-9]{1,9}$/;
@@ -47,6 +47,8 @@ export class SearchFormComponent implements OnInit {
this.handleKeyDown($event);
}
@ViewChild('searchInput') searchInput: ElementRef;
constructor(
private formBuilder: UntypedFormBuilder,
private router: Router,
@@ -55,11 +57,26 @@ export class SearchFormComponent implements OnInit {
private electrsApiService: ElectrsApiService,
private apiService: ApiService,
private relativeUrlPipe: RelativeUrlPipe,
private elementRef: ElementRef,
) { }
private elementRef: ElementRef
) {
}
ngOnInit(): void {
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
if (this.searchInput && e.type === EventType.NavigationStart) {
this.searchInput.nativeElement.blur();
}
});
this.stateService.searchFocus$.subscribe(() => {
if (!this.searchInput) { // Try again a bit later once the view is properly initialized
setTimeout(() => this.searchInput.nativeElement.focus(), 100);
} else if (this.searchInput) {
this.searchInput.nativeElement.focus();
}
});
this.searchForm = this.formBuilder.group({
searchText: ['', Validators.required],

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input, DoCheck } from '@angular/core';
import { Subscription } from 'rxjs';
import { MarkBlockState, StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
@@ -9,7 +9,7 @@ import { BlockExtended } from '../../interfaces/node-api.interface';
templateUrl: './start.component.html',
styleUrls: ['./start.component.scss'],
})
export class StartComponent implements OnInit, OnDestroy {
export class StartComponent implements OnInit, OnDestroy, DoCheck {
@Input() showLoadingIndicator = false;
interval = 60;
@@ -43,6 +43,7 @@ export class StartComponent implements OnInit, OnDestroy {
pageIndex: number = 0;
pages: any[] = [];
pendingMark: number | null = null;
pendingOffset: number | null = null;
lastUpdate: number = 0;
lastMouseX: number;
velocity: number = 0;
@@ -54,6 +55,14 @@ export class StartComponent implements OnInit, OnDestroy {
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);
}
ngDoCheck(): void {
if (this.pendingOffset != null) {
const offset = this.pendingOffset;
this.pendingOffset = null;
this.addConvertedScrollOffset(offset);
}
}
ngOnInit() {
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
@@ -429,6 +438,7 @@ export class StartComponent implements OnInit, OnDestroy {
addConvertedScrollOffset(offset: number): void {
if (!this.blockchainContainer?.nativeElement) {
this.pendingOffset = offset;
return;
}
if (this.timeLtr) {

View File

@@ -7,7 +7,7 @@
<div *ngIf="officialMempoolSpace">
<h2>Trademark Policy and Guidelines</h2>
<h5>The Mempool Open Source Project &trade;</h5>
<h5>The Mempool Open Source Project &reg;</h5>
<h6>Updated: July 19, 2021</h6>
<br>
@@ -304,7 +304,7 @@
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
<p>“The Mempool Space K.K.&trade;, 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, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
<p>“The Mempool Space K.K.&trade;, The Mempool Open Source Project&reg;, 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, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
<li>What to Do When You See Abuse</li>

View File

@@ -379,7 +379,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
};
const hasRelatives = !!(tx.ancestors.length || tx.bestDescendant);
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
} else {
this.fetchCpfp$.next(this.tx.txid);

View File

@@ -23,7 +23,7 @@
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
<tr [ngClass]="{
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
'highlight': this.address !== '' && (vin.prevout?.scriptpubkey_address === this.address || (vin.prevout?.scriptpubkey_type === 'p2pk' && vin.prevout?.scriptpubkey.slice(2, -2) === this.address))
}">
<td class="arrow-td">
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
@@ -56,7 +56,9 @@
<span i18n="transactions-list.peg-in">Peg-in</span>
</ng-container>
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
<span>P2PK</span>
<span>P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey.slice(2, -2)]" title="{{ vin.prevout.scriptpubkey.slice(2, -2) }}">
<app-truncate [text]="vin.prevout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
</a></span>
</ng-container>
<ng-container *ngSwitchDefault>
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
@@ -73,7 +75,7 @@
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
</ng-template>
<div>
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] || null"></app-address-labels>
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] ? tx._channels.inputs[vindex] : null"></app-address-labels>
</div>
</ng-template>
</ng-container>
@@ -182,12 +184,19 @@
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
<tr [ngClass]="{
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, -2) === this.address))
}">
<td class="address-cell">
<a class="address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<a class="address" *ngIf="vout.scriptpubkey_address; else pubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
</a>
<ng-template #pubkey_type>
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, -2)]" title="{{ vout.scriptpubkey.slice(2, -2) }}">
<app-truncate [text]="vout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
</a>
</ng-container>
</ng-template>
<div>
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
</div>

View File

@@ -140,6 +140,15 @@ h2 {
font-family: monospace;
}
.p2pk-address {
display: inline-block;
margin-left: 1em;
max-width: 100px;
@media (min-width: 576px) {
max-width: 200px
}
}
.grey-info-text {
color:#6c757d;
font-style: italic;

View File

@@ -73,12 +73,12 @@
</div>
</div>
<div class="col" style="max-height: 410px">
<div class="card">
<div class="card" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else latestBlocks">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<table class="table lastest-replacements-table">
<thead>
@@ -106,6 +106,46 @@
</table>
</div>
</div>
<ng-template #latestBlocks>
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<table class="table lastest-blocks-table">
<thead>
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
<th *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
<th *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4" i18n="mining.pool-name">Pool</th>
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
</thead>
<tbody>
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
onError="this.src = '/resources/mining-pools/default.svg'">
<span class="pool-name">{{ block.extras.pool.name }}</span>
</a>
</td>
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
<td class="table-cell-size">
<div class="progress">
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }">&nbsp;</div>
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-template>
</div>
<div class="col" style="max-height: 410px">
<div class="card">
@@ -180,10 +220,10 @@
<ng-template #mempoolTable let-mempoolInfoData>
<div class="mempool-info-data">
<div class="item">
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.00001 || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === mempoolInfoData.value.memPoolInfo.minrelaytxfee || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
<ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee > 0.00001">&lt; </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee !== mempoolInfoData.value.memPoolInfo.minrelaytxfee">&lt; </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
</p>
</div>
<div class="item">

View File

@@ -175,6 +175,43 @@
height: 18px;
}
.lastest-blocks-table {
width: 100%;
text-align: left;
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
}
.table-cell-height {
width: 15%;
}
.table-cell-mined {
width: 35%;
text-align: left;
}
.table-cell-transaction-count {
display: none;
text-align: right;
width: 20%;
display: table-cell;
}
.table-cell-size {
display: none;
text-align: center;
width: 30%;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: table-cell;
}
}
}
.lastest-replacements-table {
width: 100%;
text-align: left;

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
import { filter, map, scan, share, switchMap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { catchError, filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service';
@@ -31,7 +31,7 @@ interface MempoolStatsData {
styleUrls: ['./dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent implements OnInit, OnDestroy {
export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
featuredAssets$: Observable<any>;
network$: Observable<string>;
mempoolBlocksData$: Observable<MempoolBlocksData>;
@@ -39,6 +39,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
mempoolLoadingStatus$: Observable<number>;
vBytesPerSecondLimit = 1667;
transactions$: Observable<TransactionStripped[]>;
blocks$: Observable<BlockExtended[]>;
replacements$: Observable<ReplacementInfo[]>;
latestBlockHeight: number;
mempoolTransactionsWeightPerSecondData: any;
@@ -56,6 +57,10 @@ export class DashboardComponent implements OnInit, OnDestroy {
private seoService: SeoService
) { }
ngAfterViewInit(): void {
this.stateService.focusSearchInputDesktop();
}
ngOnDestroy(): void {
this.currencySubscription.unsubscribe();
this.websocketService.stopTrackRbfSummary();
@@ -144,12 +149,33 @@ export class DashboardComponent implements OnInit, OnDestroy {
}, []),
);
this.blocks$ = this.stateService.blocks$
.pipe(
tap((blocks) => {
this.latestBlockHeight = blocks[0].height;
}),
switchMap((blocks) => {
if (this.stateService.env.MINING_DASHBOARD === true) {
for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template
block.extras.pool.logo = `/resources/mining-pools/` +
block.extras.pool.slug + '.svg';
}
}
return of(blocks.slice(0, 6));
})
);
this.replacements$ = this.stateService.rbfLatestSummary$;
this.mempoolStats$ = this.stateService.connectionState$
.pipe(
filter((state) => state === 2),
switchMap(() => this.apiService.list2HStatistics$()),
switchMap(() => this.apiService.list2HStatistics$().pipe(
catchError((e) => {
return of(null);
})
)),
switchMap((mempoolStats) => {
return merge(
this.stateService.live2Chart$
@@ -164,10 +190,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
);
}),
map((mempoolStats) => {
return {
mempool: mempoolStats,
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
};
if (mempoolStats) {
return {
mempool: mempoolStats,
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
};
} else {
return null;
}
}),
share(),
);
@@ -206,16 +236,4 @@ export class DashboardComponent implements OnInit, OnDestroy {
trackByBlock(index: number, block: BlockExtended) {
return block.height;
}
checkFullRbf(tree: RbfTree): void {
let fullRbf = false;
for (const replaced of tree.replaces) {
if (!replaced.tx.rbf) {
fullRbf = true;
}
replaced.replacedBy = tree.tx;
this.checkFullRbf(replaced);
}
tree.tx.fullRbf = fullRbf;
}
}

View File

@@ -8936,8 +8936,8 @@ export const faqData = [
type: "endpoint",
category: "self-hosting",
showConditions: bitcoinNetworks,
fragment: "host-my-own-instance-linux-server",
title: "How can I host my own instance on a Linux server?",
fragment: "host-my-own-instance-server",
title: "How can I host a Mempool instance on my own server?",
},
{
type: "endpoint",

View File

@@ -40,7 +40,7 @@
<div class="doc-content">
<p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title">REST API service</ng-container>.</p>
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a [routerLink]="['/enterprise']">enterprise sponsorship</a> if you need higher API limits.</p>
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="/enterprise">enterprise sponsorship</a> if you need higher API limits.</p>
<div class="doc-item-container" *ngFor="let item of restDocs">
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
@@ -279,8 +279,9 @@
We support one-click installation on a number of Raspberry Pi full-node distros including Umbrel, RaspiBlitz, MyNode, RoninDojo, and Start9's Embassy.
</ng-template>
<ng-template type="host-my-own-instance-linux-server">
You can manually install Mempool on your own Linux server, but this requires advanced sysadmin skills since you will be manually configuring everything. We do not provide support for manual deployments.
<ng-template type="host-my-own-instance-server">
<p>You can manually install Mempool on your own server, but this requires advanced sysadmin skills since you will be manually configuring everything. You could also use our <a href="https://github.com/mempool/mempool/tree/master/docker" target="_blank">Docker images</a>.</p><p>In any case, we only provide support for manual deployments to <a href="/enterprise">enterprise sponsors</a>.</p>
<p>For casual users, we strongly suggest installing Mempool using one of the <a href="https://github.com/mempool/mempool#one-click-installation" target="_blank">1-click install methods</a>.</p>
</ng-template>
<ng-template type="install-mempool-with-docker">

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