Compare commits

..

1286 Commits

Author SHA1 Message Date
wiz
541555051b Merge branch 'master' into knorrium/update-node-matrix 2025-01-20 19:26:56 +09:00
wiz
5aeaa68259 ops: Enable stratum in FOSS prod frontend config 2025-01-20 17:56:56 +09:00
nymkappa
e01898a4c5 Merge pull request #5640 from mempool/nymkappa/square-errors
[accelerator] display payment errors, auto reload after 10 secs instead of 3 secs
2025-01-20 17:47:18 +09:00
nymkappa
0568a8c6c1 Merge branch 'master' into nymkappa/square-errors 2025-01-20 17:36:31 +09:00
wiz
e53e810a55 ops: Fix stratum server URL path in prod config 2025-01-20 17:34:19 +09:00
nymkappa
e2c44b6c62 Merge branch 'master' into nymkappa/square-errors 2025-01-20 17:23:15 +09:00
wiz
36b691e25b ops: Enable stratum in prod config via localhost nginx proxy 2025-01-20 17:20:38 +09:00
Felipe Knorr Kuhn
5f5e96984a Merge branch 'master' into knorrium/update-node-matrix 2025-01-19 23:41:46 -08:00
wiz
4a14e8d921 Merge pull request #5736 from mempool/mononaut/fix-stratum-trees
fix stratum tree rendering with different branch lengths
2025-01-20 16:39:56 +09:00
Mononaut
4e735cc8b0 fix stratum tree rendering with different branch lengths 2025-01-20 07:30:27 +00:00
softsimon
4520e3fdf2 Merge pull request #5735 from mempool/nymkappa/new-fa-icon
add new fa icon
2025-01-20 14:01:37 +07:00
nymkappa
390bbf1097 add new fa icon 2025-01-20 15:20:29 +09:00
wiz
bd8c1efc8e Merge pull request #5734 from mempool/knorrium/update_stg_hosts 2025-01-20 13:07:31 +09:00
Felipe Knorr Kuhn
8fbc497a58 Merge branch 'knorrium/update_stg_hosts' of github.com:mempool/mempool into knorrium/update_stg_hosts 2025-01-19 19:50:25 -08:00
Felipe Knorr Kuhn
003956fd16 Update staging hosts
Add rerouting logic for testnet4 tests
Split CI e2e workflow matrix into mempool, liquid and testnet4
2025-01-19 19:50:05 -08:00
Felipe Knorr Kuhn
227d99e990 Fix reroute logic 2025-01-19 19:35:08 -08:00
Felipe Knorr Kuhn
3d1aacbd66 Copypasta matrix for tests 2025-01-19 19:21:02 -08:00
Felipe Knorr Kuhn
1098d2fe3c Change build step to shell script 2025-01-19 14:21:23 -08:00
Felipe Knorr Kuhn
f59e95fcc8 Run the default mempool config if we are running testnet4 tests 2025-01-19 14:11:41 -08:00
Felipe Knorr Kuhn
7f6399093e Fix YAML 2025-01-19 13:56:16 -08:00
Felipe Knorr Kuhn
e9e8b0c758 Use testnet as a matrix config instead 2025-01-19 13:38:06 -08:00
Felipe Knorr Kuhn
517a30d2b0 Split module and spec matrix 2025-01-19 13:25:29 -08:00
Felipe Knorr Kuhn
7e766cc28d Change spec test 2025-01-19 12:36:20 -08:00
Felipe Knorr Kuhn
34099e3861 Stop switching to testnet4 until we can check for the proxied server 2025-01-19 12:32:37 -08:00
Felipe Knorr Kuhn
671b5ea2f2 Reroute testnet4 tests to a different server 2025-01-19 12:24:26 -08:00
Felipe Knorr Kuhn
caa2d83247 Update staging hosts 2025-01-19 02:34:05 -08:00
wiz
703241acf0 Merge pull request #5509 from mempool/mononaut/pool-next-block
mining pool next block info
2025-01-19 17:00:38 +09:00
wiz
6e8579363d Merge pull request #5508 from mempool/mononaut/stratum
stratum job visualization
2025-01-19 16:48:48 +09:00
Mononaut
b254be2f49 add stratum job to pool page 2025-01-19 06:02:58 +00:00
Mononaut
d6283c54ee fix stratum config 2025-01-19 05:42:40 +00:00
Mononaut
9ba7172b5b add coinbase tag column to stratum table 2025-01-19 05:42:40 +00:00
Mononaut
cb4bf0611e add blockchain bar to stratum page 2025-01-19 05:42:40 +00:00
Mononaut
3ea491ad13 stratum frontend config 2025-01-19 05:42:40 +00:00
Mononaut
eddd7344ad stratum backend config 2025-01-19 05:42:39 +00:00
Mononaut
4ecf2eb679 experimental stratum job visualization 2025-01-19 05:41:48 +00:00
wiz
34acbca4b9 Merge pull request #5720 from mempool/nymkappa/missing-icon
add new robot icon
2025-01-17 17:14:47 +09:00
wiz
8793fafa4c Merge pull request #5704 from mempool/mononaut/sca-loading-ux
[accelerator] improve SCA UX
2025-01-17 17:04:52 +09:00
softsimon
341da85c77 Merge pull request #5726 from mempool/mononaut/close-time-rift
Fix time traveling balance charts
2025-01-17 10:56:22 +07:00
softsimon
0d8f63feff Merge pull request #5725 from mempool/mononaut/fix-partial-rbf
avoid creating incomplete RBF trees
2025-01-17 10:36:46 +07:00
softsimon
e7af43efa2 Merge pull request #5723 from mempool/mononaut/fix-cached-tx-badge
Fix unconfirmed badge on broken RBF txs
2025-01-17 10:15:35 +07:00
Felipe Knorr Kuhn
f5a54ae62b Update the local nvmrc to node 22 2025-01-15 21:52:54 -08:00
Felipe Knorr Kuhn
4da145ff5c Update the node version to run e2e tests 2025-01-15 21:52:38 -08:00
Felipe Knorr Kuhn
a898701830 Update node matrix to v20 and v22 2025-01-15 21:33:25 -08:00
wiz
aca2f2ec7d ops: Set expires -1 header on 2s server cached responses 2025-01-15 16:34:56 +09:00
wiz
803b005880 Merge pull request #5562 from mempool/mononaut/wallet-transactions
Wallet page transactions
2025-01-15 16:33:57 +09:00
Mononaut
204d54b189 fix wallet transactions ordering 2025-01-15 02:39:31 +00:00
Mononaut
c248544fe8 Update wallet page title 2025-01-15 02:39:31 +00:00
Mononaut
b65d00f289 switch multi-address APIs to use POST 2025-01-15 02:39:30 +00:00
Mononaut
f77dc68ec7 Add link to wallet page from custom dashboard txs widget 2025-01-15 02:39:30 +00:00
Mononaut
c4ec50b771 Restore transactions list to wallet page 2025-01-15 02:39:30 +00:00
Mononaut
8529b99675 custom dashboard wallet widgets 2025-01-15 02:39:29 +00:00
Mononaut
cd02d89235 Fix time traveling balance charts 2025-01-14 09:18:35 +00:00
Mononaut
4dcbccd9b2 avoid creating incomplete RBF trees 2025-01-14 06:41:34 +00:00
Mononaut
6a4aeaf7ed Fix unconfirmed badge on broken RBF txs 2025-01-14 06:31:35 +00:00
softsimon
6432f72664 Merge pull request #5724 from mempool/natsoni/textarea-nav
Fix textarea keyboard navigation
2025-01-14 12:25:24 +07:00
natsoni
f6ab2caaf9 Fix textarea keyboard navigation 2025-01-14 10:42:40 +09:00
softsimon
0a255d7fe5 Merge pull request #5706 from mempool/natsoni/websocket-docs
Add websocket commands doc
2025-01-13 06:55:15 +07:00
softsimon
ca0a8aee49 Merge pull request #5716 from mempool/natsoni/fix-address-amount
Fix transaction amount change for P2PK addresses
2025-01-13 06:36:46 +07:00
nymkappa
4a4259fa7d add new robot icon, wrap import into new lines 2025-01-12 12:16:51 +09:00
nymkappa
f142b421f9 Merge pull request #5623 from mempool/nymkappa/delete-irrelevant-code
[refactor] cleaning users.full_name
2025-01-12 11:38:23 +09:00
nymkappa
47cc58c610 Merge branch 'master' into mononaut/sca-loading-ux 2025-01-12 11:11:29 +09:00
softsimon
c3686a5500 Merge pull request #5713 from mempool/mononaut/fix-missing-unsubs
clean up subscriptions & component references
2025-01-09 16:48:44 +07:00
natsoni
9fbbe4980d Fix transaction amount change for P2PK addresses 2025-01-08 16:01:59 +01:00
softsimon
0611773647 Merge pull request #5715 from mempool/natsoni/fix-first-seen-loop
Fix filter logic for first seen API fetching
2025-01-08 21:28:11 +07:00
Mononaut
7740908a4c fix access block scene after destroyed 2025-01-08 13:08:33 +00:00
natsoni
68ea7c59f3 Fix transaction filter logic for first seen fetching 2025-01-07 11:21:04 +01:00
wiz
915f7a6c27 Merge pull request #5714 from mempool/nymkappa/missing-icon
add missing icon
2025-01-07 16:34:46 +09:00
nymkappa
e18c572549 add missing icon 2025-01-07 16:31:03 +09:00
wiz
25133d8505 Merge pull request #5708 from mempool/nymkappa/sca-notification
[accelerator] print sca status for google payment
2025-01-07 15:56:35 +09:00
Mononaut
9f5666f410 explicitly destroy block scenes 2025-01-06 20:44:28 +00:00
Mononaut
6553344489 add missing rxjs unsubscriptions 2025-01-06 18:55:40 +00:00
softsimon
3c84505579 upgrading fortawesome dep 2024-12-27 18:44:37 +07:00
softsimon
81315af206 Merge pull request #5710 from mempool/simon/remove-lightweight-charts
remove unused lightweight-charts
2024-12-27 17:13:51 +07:00
softsimon
6fa747b303 remove unused lightweight-charts 2024-12-27 15:58:53 +07:00
nymkappa
37ddc29c2c [accelerator] print sca status for google payment 2024-12-25 16:38:53 +08:00
nymkappa
c66f028f12 Merge pull request #5702 from mempool/nymkappa/fix-response-header
fix duplicated response header
2024-12-23 15:38:11 +08:00
natsoni
a5c67b5ca1 Add websocket commands doc 2024-12-22 20:24:20 +01:00
Mononaut
f49152d09d [accelerator] keep checkout locked until request completes 2024-12-22 15:34:32 +00:00
Mononaut
464fabf137 [accelerator] reference counting for checkout lock 2024-12-22 15:34:03 +00:00
nymkappa
ed28a24c8a fix duplicated response header 2024-12-22 22:33:54 +08:00
Mononaut
ba1ee15286 [accelerator] improve SCA UX 2024-12-22 12:27:29 +00:00
softsimon
ddcf745722 Merge pull request #5703 from mempool/mononaut/revert-difficulty-timeavg
revert difficulty widget to true avg block time
2024-12-22 18:19:09 +07:00
Mononaut
774c0b4f83 revert difficulty widget to true avg block time 2024-12-22 10:12:00 +00:00
softsimon
734d5f2461 Merge pull request #5579 from mempool/natsoni/use-adjusted-time-avg
Use adjusted block time for difficulty and ETA calculation
2024-12-22 15:59:52 +07:00
softsimon
24a76cafa4 Merge branch 'master' into natsoni/use-adjusted-time-avg 2024-12-21 22:10:04 +07:00
softsimon
348a12c4a1 Merge pull request #5600 from mempool/natsoni/tx-input-overflow
Fix input/output overflow in transaction list
2024-12-21 22:09:30 +07:00
softsimon
67750bd166 Merge branch 'master' into natsoni/tx-input-overflow 2024-12-21 17:41:27 +07:00
softsimon
e5ea7afbe2 Merge pull request #5637 from mempool/natsoni/timezone-selection
Add timezone selector
2024-12-21 16:27:32 +07:00
softsimon
4b56144e6e Merge pull request #5701 from mempool/natsoni/fix-inscription-badge
Fix inscription badge disappearing when loading more inputs
2024-12-21 16:07:22 +07:00
wiz
332099cc01 Merge pull request #5700 from mempool/mononaut/standardize-api-errors
standardize API error strings & validation
2024-12-20 12:58:15 +09:00
natsoni
0a933d022c Fix inscription disappearing when loading more inputs 2024-12-19 15:01:58 +01:00
Mononaut
47044db043 standardize API error strings & validation 2024-12-19 13:00:44 +00:00
wiz
01df22ef86 Merge pull request #5682 from mempool/nymkappa/googlepaysca
[accelerator] add sca for googlepay payments
2024-12-18 11:26:20 +09:00
wiz
6d4f03e5f2 Merge pull request #5683 from mempool/mononaut/fix-liquid-monitoring
fix liquid monitoring url routes
2024-12-14 15:16:13 +09:00
wiz
2d2f3ad4c4 Merge pull request #5687 from mempool/natsoni/fix-package-broadcast-css
Fix package broadcast table css
2024-12-14 15:16:01 +09:00
wiz
70036e4a7e Merge pull request #5689 from mempool/mononaut/fix-monitoring-hash-urls
fix monitoring git hash urls
2024-12-14 15:15:50 +09:00
wiz
4daa997e58 Merge pull request #5690 from mempool/mononaut/fix-unfurler-meta-title
fix unfurler meta titles
2024-12-14 15:15:39 +09:00
wiz
15dd4cd633 Merge pull request #5680 from mempool/natsoni/unify-db-schema
Unify database schema for all backend types
2024-12-14 12:18:12 +09:00
Mononaut
8b73bdfba9 fix unfurler meta titles 2024-12-13 22:09:14 +00:00
Mononaut
4fe246ecf1 fix monitoring git hash urls 2024-12-13 16:16:18 +00:00
wiz
90bb5304ef ops: Only build backends that actually exist 2024-12-13 09:53:51 +09:00
wiz
ded47eb309 Merge pull request #5684 from mempool/mononaut/monitoring-hash
add git hashes to monitoring page
2024-12-13 09:52:54 +09:00
wiz
aef361b01a Merge pull request #5676 from mempool/mononaut/balance-chart-ticks
Fix fiat tick precision on address balance chart
2024-12-13 09:50:09 +09:00
natsoni
392f6a01c4 Fix package broadcast table css 2024-12-12 19:22:03 +01:00
Mononaut
6112c7f8ee Add git hashes to monitoring 2024-12-12 00:52:28 +00:00
Mononaut
58b4c07924 fix liquid monitoring url routes 2024-12-10 22:19:57 +00:00
nymkappa
ad360db71f [accelerator] add sca for googlepay payments 2024-12-10 15:16:19 +01:00
wiz
e58579ed8a Merge pull request #5681 from mempool/mononaut/wallet-preview
wallet unfurler preview
2024-12-10 13:18:41 +09:00
wiz
f57eb047f6 Merge pull request #5679 from mempool/nymkappa/cherry-pick-lol
update unfurler and build config
2024-12-10 13:18:03 +09:00
Mononaut
dcae94ba66 add wallet unfurler preview 2024-12-10 00:28:41 +00:00
Mononaut
1d2a5e9c94 refactor address graph rendering controls 2024-12-10 00:27:00 +00:00
Mononaut
522b4d914f add missing unfurler config file 2024-12-09 17:20:34 +00:00
natsoni
2372d8cff3 Unify database schema for all backend types 2024-12-09 12:08:29 +01:00
Mononaut
59cefc2b4b update unfurler and build config 2024-12-09 09:05:18 +01:00
wiz
9714789062 Add metaplanet routes to unfurler 2024-12-09 15:53:38 +09:00
wiz
13405b4494 ops: Start metaplanet unfurler process 2024-12-09 15:30:37 +09:00
wiz
6d51ce1f38 ops: Fix metaplanet unfurler config 2024-12-09 15:22:37 +09:00
wiz
bce9ea3661 Merge pull request #5677 from mempool/mononaut/enterprise-logo-center
center enterprise footer logo
2024-12-09 13:54:31 +09:00
Mononaut
05a21f3867 center enterprise footer logo 2024-12-09 03:30:06 +00:00
wiz
d50cfe135f Merge pull request #5674 from mempool/mononaut/balance-widget-usd
show USD series by default in address balance widget
2024-12-09 12:11:22 +09:00
wiz
8ae8430711 Merge pull request #5659 from mempool/nymkappa/internal-price-rest-api
[internal] provide internal rest api to retreive btcusd price history
2024-12-09 10:32:11 +09:00
wiz
12daea0f62 ops: Add metaplanet related configs 2024-12-09 10:22:46 +09:00
wiz
0d31143fed Merge pull request #5671 from mempool/natsoni/cancelled-accel-on-timeline
Canceled acceleration on timeline
2024-12-08 13:12:27 +09:00
Mononaut
526625fc56 Fix fiat tick precision on address balance chart 2024-12-07 20:38:02 +00:00
Mononaut
7f3cdbfdb6 show USD series by default in address balance widget 2024-12-07 16:01:26 +00:00
nymkappa
5be4346dc1 Merge branch 'master' into nymkappa/internal-price-rest-api 2024-12-06 10:17:52 +01:00
wiz
a2bc6f5bba Trim string for Hawaii Standard Time 2024-12-06 17:20:16 +09:00
wiz
a0596cd366 Merge pull request #5654 from mempool/natsoni/address-graph-fix
Fix address balance graph
2024-12-06 17:17:08 +09:00
wiz
0f14aa7ad3 Merge pull request #5635 from mempool/natsoni/refactor-clipboard
Refactor clipboard component
2024-12-06 17:16:26 +09:00
wiz
44a0f92cc1 Merge pull request #5627 from mempool/mononaut/custom-dash-assets
update custom dashboard assets
2024-12-06 17:15:47 +09:00
wiz
97a9ea47fc Merge pull request #5641 from mempool/mononaut/fix-mempool-network-change
fix stuck mempool block on network change
2024-12-06 17:14:35 +09:00
wiz
d573147ad4 Remove unnecessary par=16 from bitcoin.conf 2024-12-06 15:14:26 +09:00
wiz
679745fb6c Merge pull request #5668 from mempool/natsoni/fix-accel-cancellation
Fix tx frontend issues after acceleration cancellation
2024-12-06 14:30:04 +09:00
wiz
c089920e4b Merge pull request #5666 from mempool/mononaut/fix-undefined-mempooltxs
fix undefined mempool tx errors
2024-12-06 14:29:47 +09:00
softsimon
62c96272d8 Merge pull request #5669 from mempool/natsoni/fix-liquid
Fix liquid database index
2024-12-05 21:55:35 +07:00
natsoni
d87b668353 Show timeline on canceled accelerations 2024-12-05 12:36:17 +01:00
natsoni
ebd4170da1 Fix liquid database index 2024-12-04 16:14:51 +01:00
natsoni
0310452dfb Fix tx frontend issues after acceleration cancellation 2024-12-04 15:45:18 +01:00
wiz
969687ef39 Merge pull request #5662 from mempool/nymkappa/remove-useless-unique-uuid-accel
[accelerator] remove useless accelerationUUID
2024-12-04 17:15:09 +09:00
wiz
0831256cce Merge pull request #5663 from mempool/nymkappa/acceleration-cancel
[doc] add accelerator cancel doc
2024-12-04 16:41:54 +09:00
wiz
af40cac284 Merge pull request #5643 from mempool/mononaut/liquid-db-indexes
liquid db indexes
2024-12-04 16:37:40 +09:00
Mononaut
e4868b70c1 more processBlockTemplates null checks 2024-12-02 22:51:24 +00:00
Mononaut
5f45ce80f1 filter accelerations before calculating pool positions 2024-12-02 22:51:23 +00:00
softsimon
14e49126c3 Merge pull request #5642 from mempool/mononaut/db-indexes
Add missing db indexes
2024-11-28 15:03:43 +07:00
nymkappa
a4d73130b7 [doc] add accelerator cancel doc 2024-11-26 17:30:11 +01:00
nymkappa
cd702955fc [accelerator] remove useless accelerationUUID 2024-11-26 17:14:47 +01:00
wiz
4c66bf61f0 Merge pull request #5649 from mempool/mononaut/fix-acc-ws-timeout
fix acceleration websocket timeout loop
2024-11-22 07:21:10 -10:00
Mononaut
ffa582558b more verbose accelerator websocket error logs 2024-11-21 21:54:19 +00:00
nymkappa
423b41939e [internal] provide internal rest api to retreive btcusd price history 2024-11-21 15:20:24 +01:00
Mononaut
9a81db8e6c fix acceleration websocket ping error 2024-11-19 23:00:28 +00:00
natsoni
cb3326d691 Wrap large amounts in power of ten in address graph 2024-11-19 19:32:28 +01:00
natsoni
535e5313ef Polish address balance graph tooltip 2024-11-19 18:11:02 +01:00
natsoni
8bd6d40ed2 Don't use SI units in address balance graph axis 2024-11-19 18:00:00 +01:00
natsoni
7516db0c71 Fix USD y axis overflow in address graph 2024-11-19 17:45:09 +01:00
Mononaut
abe9aa1fdc fix acceleration websocket timeout loop 2024-11-18 20:40:08 +00:00
Mononaut
60b3f9ace6 update custom dashboard assets 2024-11-17 16:05:48 +00:00
wiz
073fe8e8cd Merge pull request #5645 from mempool/nymkappa/accelerator-back-button-payment 2024-11-16 19:28:10 +09:00
nymkappa
bfe7b996a4 [accelerator] fix "Go back" button breaking payment flow 2024-11-16 11:23:09 +01:00
Mononaut
bfd771056d split new liquid db indexes into separate migrations 2024-11-16 00:01:13 +00:00
Mononaut
e05f5ee751 Add missing liquid db indexes 2024-11-16 00:00:18 +00:00
Mononaut
d9f3611da3 split new db indexes into separate migrations 2024-11-15 23:54:26 +00:00
Mononaut
7c7419ab1c Add missing db indexes 2024-11-15 22:30:46 +00:00
Mononaut
96afbca029 fix stuck mempool block on network change 2024-11-15 01:34:06 +00:00
softsimon
8c80358e71 Merge pull request #5634 from mempool/simon/fix-broken-cpfp-button
fix broken cpfp button
2024-11-15 08:29:02 +07:00
natsoni
9e5b7436d4 Add timezone selector 2024-11-14 17:10:30 +01:00
wiz
a5fbc94182 Merge pull request #5638 from mempool/mononaut/fix-acc-list-sub 2024-11-15 00:51:18 +09:00
nymkappa
72ddb8c6a4 [accelerator] display payment errors, auto reload after 10 secs instead of 3 secs 2024-11-14 16:46:18 +01:00
Mononaut
fd7f340854 Fix acceleration list observable subscription logic 2024-11-14 02:40:41 +00:00
wiz
7f784944af ops: Fix install script nginx config parse error 2024-11-14 07:37:01 +09:00
natsoni
5a3ee725b8 Use timestamp component 2024-11-13 14:43:04 +01:00
natsoni
cab01f7f26 Refactor clipboard to use native clipboard API 2024-11-12 17:09:58 +01:00
softsimon
3b4eda432f fix cpfp list title position 2024-11-12 03:22:16 +07:00
softsimon
8b699da721 fix broken cpfp button 2024-11-12 03:18:22 +07:00
wiz
5b2f613856 Merge pull request #5633 from mempool/mononaut/fix-acc-ping-pong
Fix acceleration websocket ping/pong
2024-11-12 02:25:15 +09:00
Mononaut
f1e2c893cc Fix acceleration websocket ping/pong 2024-11-11 17:23:30 +00:00
wiz
7d3d59c348 Merge pull request #5632 from mempool/mononaut/acc-ws-ping
regularly ping acceleration websocket server
2024-11-12 02:08:48 +09:00
wiz
8719b424e5 Increase websocket from 5s to 10s 2024-11-12 02:03:56 +09:00
Mononaut
ef498b55ed reset acceleration websocket if unresponsive 2024-11-11 16:59:08 +00:00
Mononaut
9718610104 regularly ping acceleration websocket server 2024-11-11 16:26:30 +00:00
wiz
937e82bb89 Merge pull request #5631 from mempool/mononaut/acc-ws-debug
Better debug logs for accelerator websocket
2024-11-12 00:32:54 +09:00
Mononaut
91bf35bb65 Better debug logs for accelerator websocket 2024-11-11 14:40:25 +00:00
wiz
f0f6ee1919 Merge pull request #5630 from mempool/mononaut/fix-acc-websocket
fix acceleration websocket protocol
2024-11-11 20:53:27 +09:00
Mononaut
7b837b96da fix acceleration websocket protocol 2024-11-11 11:46:17 +00:00
softsimon
c417470be2 Merge pull request #5621 from mempool/natsoni/tx-first-seen-fix
Show tx first seen time with audit disabled
2024-11-09 00:28:47 +07:00
softsimon
dfd7877f82 Merge pull request #5584 from mempool/simon/cpfp-polish
Polish CPFP button
2024-11-09 00:25:05 +07:00
softsimon
136e80e5cf Merge pull request #5625 from mempool/natsoni/fix-url-nav
Fix navigation to use relative paths
2024-11-09 00:08:52 +07:00
natsoni
b3aed2f58b Fix navigation to use relative paths 2024-11-02 10:41:37 +01:00
wiz
e75f913af3 ops: Cache testnet electrs endpoints for minimum of 1s 2024-11-01 21:20:54 +09:00
wiz
0a9703f164 ops: Cache all electrs endpoints for minimum of 1s 2024-11-01 21:15:07 +09:00
nymkappa
cdc4a430cd [refactor] cleaning users.full_name 2024-10-31 09:52:34 +01:00
natsoni
db321c3fa5 Get tx first seen from block summary if not available in audit 2024-10-30 15:43:28 +01:00
natsoni
b6aeb5661f Add block/:hash/tx/:txid/summary endpoint 2024-10-30 15:31:01 +01:00
natsoni
f08fa034cc Add missing frontend audit flag for testnet4 2024-10-30 15:19:46 +01:00
wiz
c0ef01d4da Merge pull request #5617 from vostrnad/fix-missing-fakescripthash
Add missing fake_scripthash to the data filter
2024-10-30 19:11:08 +09:00
wiz
c6711d8191 Merge pull request #5619 from mempool/nymkappa/fix-zindex-loading-indicator
[ui] fix loading indicator zindex
2024-10-30 19:10:51 +09:00
wiz
216bd5ad23 Merge pull request #5618 from mempool/nymkappa/fix-pool-blocks-page-length
[mining] return 100 blocks per page instead of 10 for pool block list
2024-10-30 19:10:37 +09:00
wiz
a7d59d6b2e Merge pull request #5620 from mempool/nymkappa/pool-hashrate 2024-10-30 18:51:36 +09:00
nymkappa
a257bcc12a [mining] show pools estimated hashrate on 3d and 1w timeframes 2024-10-30 10:46:44 +01:00
wiz
66c0ea7ca3 Merge pull request #5500 from mempool/mononaut/esplora-monitoring-reorg
Fix reorg to lower height on /monitoring status page
2024-10-30 18:28:35 +09:00
wiz
7692b2af66 Merge pull request #5511 from mempool/mononaut/summary-indexing-limit
respect INDEXING_BLOCKS_AMOUNT during summary indexing
2024-10-30 18:27:26 +09:00
wiz
397f53f42d Merge branch 'master' into mononaut/esplora-monitoring-reorg 2024-10-30 18:18:16 +09:00
wiz
2ea76d9c38 Merge pull request #5394 from mempool/mononaut/use-acceleration-websocket
[accelerator] get acceleration updates over websocket
2024-10-30 18:17:26 +09:00
nymkappa
59ac27b104 [ui] fix loading indicator zindex 2024-10-30 09:59:41 +01:00
nymkappa
d27bb7e156 [mining] return 100 blocks per page instead of 10 for pool block list 2024-10-30 09:54:13 +01:00
Vojtěch Strnad
4eadfc0a3b Add missing fake_scripthash to the data filter 2024-10-29 07:25:36 +01:00
wiz
76cfa3ca47 Merge pull request #5615 from mempool/mononaut/adjust-app-imports 2024-10-27 13:55:58 +09:00
Mononaut
3a4a4d9ffd don't allow overriding critical @app imports 2024-10-27 02:39:55 +00:00
wiz
8b01a83948 Increase time of demo mode from 3s to 15s 2024-10-25 16:49:24 +09:00
wiz
2ceb9001a1 Merge pull request #5613 from mempool/mononaut/fix-purple-pie-chart
fix purple pie chart with single pool
2024-10-25 15:13:06 +09:00
wiz
d44b7926d2 Merge pull request #5611 from mempool/nymkappa/demo-mode-but-it-does-not-crash-the-tv-anymore-well-maybe-we-ll-see-about-this
[demo] better? demo mode
2024-10-25 14:48:59 +09:00
wiz
57299e086e Remove /graphs from demo routes 2024-10-25 14:45:45 +09:00
Mononaut
c1d17dac43 fix purple pie chart with single pool 2024-10-25 05:39:52 +00:00
wiz
cb49f9d929 Merge pull request #5612 from mempool/nymkappa/internal-core-rpc-proxy
[core routes] /api/internal -> /api/v1/internal
2024-10-25 14:09:03 +09:00
nymkappa
185be3d598 [core routes] use config.MEMPOOL.API_URL_PREFIX 2024-10-25 14:02:09 +09:00
softsimon
aa9888a2fe Related Transactions 2024-10-25 11:58:01 +07:00
nymkappa
c950e3d0ae [core routes] /api/internal -> /api/v1/internal 2024-10-25 13:54:40 +09:00
softsimon
3909148d6e Polish CPFP button 2024-10-25 11:52:01 +07:00
nymkappa
99cc47cf00 [demo] better? demo mode 2024-10-24 16:52:47 +09:00
wiz
908b8b4352 Merge pull request #5606 from mempool/nymkappa/configurable-prod-domains
make prod domains configurable
2024-10-23 22:58:56 +09:00
nymkappa
1a7f475220 make prod domains configurable 2024-10-23 22:51:04 +09:00
wiz
cb63d17a2f ops: Don't always set frameoptions in nginx 2024-10-23 22:12:40 +09:00
wiz
c8ce4631e2 Merge pull request #5605 from mempool/mononaut/tx-extras-module
Refactor transaction page component
2024-10-23 22:00:39 +09:00
Mononaut
96c2b0a2f7 fix cpfp button 2024-10-23 12:28:40 +00:00
Mononaut
23475c7a1b Refactor transaction page component 2024-10-23 08:56:27 +00:00
wiz
9ab3d3195e Merge pull request #5604 from mempool/wiz/use-typescript-buildtime-path-aliases
Use typescript path aliases for build time import path resolution
2024-10-23 12:01:36 +09:00
wiz
a22d07ae60 Rename some more relative paths to use @app path alias 2024-10-23 11:46:33 +09:00
wiz
221658f6bf Change @app/interfaces to new @interfaces path alias 2024-10-23 11:09:38 +09:00
wiz
133df2e4be Use typescript path aliases for build time import path resolution 2024-10-22 23:30:19 +09:00
wiz
5fba9595af Merge pull request #5601 from mempool/nymkappa/demo-mode
implement very simple demo mode
2024-10-22 15:16:45 +09:00
wiz
90ca77a46a Tweak demo mode to use all dashboards 2024-10-22 15:08:57 +09:00
nymkappa
f0c76c1349 implement very simple demo mode 2024-10-19 16:07:09 +09:00
wiz
6e5cfa9bf2 Merge pull request #4831 from mempool/mononaut/wallet
Add multi-address wallet page
2024-10-18 13:05:17 +09:00
wiz
9ffcf2eca5 ops: Enable wallets in prod mempool backend config 2024-10-18 12:32:42 +09:00
wiz
8514fb9bdc Merge branch 'master' into mononaut/wallet 2024-10-18 12:08:05 +09:00
wiz
c1be7460c0 Merge pull request #5375 from mempool/mononaut/wallet-dashboard-widgets
custom wallet dashboard widgets
2024-10-18 12:06:02 +09:00
Mononaut
602aa4f948 fix wallet merge conflicts 2024-10-18 03:02:30 +00:00
Mononaut
2d2c55ce0e Add link to wallet page from custom dashboard widget 2024-10-18 02:41:44 +00:00
Mononaut
f0e207dff2 fix wallet balance graph bug 2024-10-18 02:41:44 +00:00
Mononaut
756e4356a5 named wallet sync track txo stats 2024-10-18 02:41:44 +00:00
Mononaut
9c303e8c23 address wallet page by name 2024-10-18 02:41:43 +00:00
Mononaut
a7ba4a0be8 Add multi-address wallet page 2024-10-18 02:41:18 +00:00
Mononaut
54c2d7efe5 move custom wallet sync to services backend 2024-10-18 02:35:19 +00:00
Mononaut
3f8eb3a2cd check in new resources 2024-10-18 02:35:19 +00:00
Mononaut
e095192968 custom dashboard wallet widgets 2024-10-18 02:35:17 +00:00
Mononaut
862c9591a1 wallet tracking backend support 2024-10-18 02:34:25 +00:00
natsoni
7a8ae7c9a6 Fix input/output overflow in transaction list 2024-10-17 16:33:36 +02:00
wiz
ca7221f8b7 Merge pull request #5594 from mempool/nymkappa/revalidate-accel
[accelerator] revalidate user choice after choosing fee option
2024-10-17 15:09:31 +09:00
wiz
8a579cc374 Merge pull request #5595 from mempool/nymkappa/hashrate-1w
[mining] use getNetworkHashPs(1008)
2024-10-17 15:08:47 +09:00
softsimon
b454959acd Merge pull request #5598 from mempool/dependabot/npm_and_yarn/frontend/tslib-2.8.0
Bump tslib from 2.7.0 to 2.8.0 in /frontend
2024-10-16 14:19:27 +09:00
dependabot[bot]
4498e14be8 Bump tslib from 2.7.0 to 2.8.0 in /frontend
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/v2.7.0...v2.8.0)

---
updated-dependencies:
- dependency-name: tslib
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-16 02:51:18 +00:00
wiz
f27a9a3c50 Merge pull request #5597 from mempool/junderw/llvm17
Use llvm17 because C sucks
2024-10-15 21:19:24 +09:00
junderw
1f0b597e2f Use llvm17 because C sucks 2024-10-15 19:43:56 +09:00
wiz
a3884b95b8 Merge pull request #5593 from mempool/simon/whale-size-increase
Whale size increase
2024-10-14 19:58:02 +09:00
softsimon
f67687b573 Merge pull request #5596 from mempool/simon/taproot-wizards
Add taproot wizards as enterprise sponsor
2024-10-14 19:54:26 +09:00
softsimon
7f4dc7eb3e Add taproot wizards as enterprise sponsor 2024-10-14 19:42:32 +09:00
nymkappa
1c4be164dd [mining] use getNetworkHashPs(1008) 2024-10-14 17:03:52 +09:00
nymkappa
450d83461c [accelerator] revalidate user choice after choosing fee option 2024-10-14 14:49:53 +09:00
softsimon
5f222f59a7 Whale size increase 2024-10-14 14:47:12 +09:00
softsimon
8dac5cff9a Merge pull request #5591 from mempool/dependabot/npm_and_yarn/frontend/multi-d3b9e25284
Bump send and browser-sync in /frontend
2024-10-14 11:33:35 +09:00
softsimon
83c7b3034b Merge pull request #5589 from mempool/dependabot/npm_and_yarn/backend/multi-9f37c16f8f
Bump cookie and express in /backend
2024-10-14 11:32:45 +09:00
dependabot[bot]
ce1babf67b Bump send and browser-sync in /frontend
Bumps [send](https://github.com/pillarjs/send) to 0.19.0 and updates ancestor dependency [browser-sync](https://github.com/BrowserSync/browser-sync). These dependencies need to be updated together.


Updates `send` from 0.16.2 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.16.2...0.19.0)

Updates `browser-sync` from 3.0.2 to 3.0.3
- [Release notes](https://github.com/BrowserSync/browser-sync/releases)
- [Changelog](https://github.com/BrowserSync/browser-sync/blob/master/changelog.js)
- [Commits](https://github.com/BrowserSync/browser-sync/compare/v3.0.2...v3.0.3)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: browser-sync
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 02:28:52 +00:00
softsimon
7ea921a5cb Merge pull request #5590 from mempool/dependabot/npm_and_yarn/frontend/multi-2c5a3fe122
Bump cookie, socket.io and express in /frontend
2024-10-14 11:28:06 +09:00
softsimon
26e3a2413d Merge pull request #5567 from mempool/natsoni/block-first-seen-audit
Store first seen time in block audit
2024-10-14 10:10:21 +09:00
wiz
8ad6c93e92 Merge pull request #5587 from mempool/simon/bump-core-28
Bump Core to v28.0
2024-10-13 23:25:34 +09:00
natsoni
198d79f149 Merge branch 'master' into natsoni/block-first-seen-audit 2024-10-13 17:41:56 +09:00
dependabot[bot]
8a72a5871d Bump cookie, socket.io and express in /frontend
Bumps [cookie](https://github.com/jshttp/cookie), [socket.io](https://github.com/socketio/socket.io) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `socket.io` from 4.7.1 to 4.8.0
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/4.7.1...socket.io@4.8.0)

Updates `express` from 4.21.0 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: socket.io
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-13 07:51:12 +00:00
dependabot[bot]
2c12f890bd Bump cookie and express in /backend
Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.1 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `express` from 4.21.0 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-13 07:50:05 +00:00
softsimon
f9300130fe Bump Core to v28.0 2024-10-13 16:00:08 +09:00
softsimon
5b557b2c12 Merge pull request #5586 from mempool/natsoni/search-bar-seconds
Include optional seconds in search bar date
2024-10-13 15:28:49 +09:00
natsoni
071e9b6c2c Include optional seconds in search bar date 2024-10-13 12:54:58 +09:00
wiz
f78971e640 Merge pull request #5547 from Emzy/ops/add-testnet4
Add testnet4 to install script
2024-10-13 11:54:05 +09:00
wiz
b86c8f7976 Merge pull request #5585 from mempool/natsoni/submit-package
Add package broadcaster to tx push page
2024-10-13 11:30:57 +09:00
wiz
2ce596a14b Merge branch 'master' into natsoni/submit-package 2024-10-13 11:15:24 +09:00
natsoni
735ed87b78 Route submitpackage calls to core on esplora backends 2024-10-13 11:14:23 +09:00
natsoni
d1741a51c9 Add submit package option to tx push page 2024-10-12 17:38:48 +09:00
natsoni
9f0b3bd769 Add submitpackage endpoint 2024-10-12 17:38:37 +09:00
wiz
41088cca09 Merge pull request #5507 from mempool/nymkappa/faucet-unverified
[faucet] show unverified warning if no email provided
2024-10-12 17:10:53 +09:00
wiz
e92ffbd501 Merge branch 'master' into nymkappa/faucet-unverified 2024-10-12 17:09:07 +09:00
natsoni
93d9538845 Fix error formatting on core only backend 2024-10-12 15:56:38 +09:00
softsimon
ae46fcafb9 Merge pull request #5583 from mempool/natsoni/inscriptions-license
Add license to inscriptions.utils.ts
2024-10-10 20:56:02 +09:00
natsoni
69a994afd5 Add license to inscriptions.utils.ts 2024-10-10 20:53:19 +09:00
wiz
c6cc533baa Merge pull request #5582 from mempool/simon/set-audit-start-heights-prod
Set audit start heights on prod
2024-10-10 20:46:10 +09:00
natsoni
dd0542bbe1 Store block first seen in db 2024-10-10 18:47:07 +09:00
softsimon
cdb4580c6d Set audit start heights on prod 2024-10-10 18:01:35 +09:00
softsimon
fe4b39df80 Merge pull request #5483 from mempool/natsoni/handle-city-states
Handle city-states in geolocation component
2024-10-10 16:22:48 +09:00
wiz
1a7519dd00 Merge pull request #5576 from mempool/nymkappa/fix-simple-mode-amount-charged
[accelerator] fee delta matches what the user accepted to pay in frontend
2024-10-10 15:59:41 +09:00
softsimon
5116a27e8d Merge pull request #5581 from mempool/natsoni/fix-timeline-again
Fix timespan on acceleration timeline
2024-10-10 10:23:41 +09:00
natsoni
73e8ba3e47 Fix timestamps on acceleration timeline 2024-10-09 20:52:23 +09:00
softsimon
6805b673fa Merge pull request #5580 from mempool/natsoni/fix-frontend
Fix frontend build
2024-10-09 18:18:25 +09:00
natsoni
22236bdabe Fix frontend build 2024-10-09 18:17:17 +09:00
softsimon
05f60cda56 Merge pull request #5578 from mempool/natsoni/fix-timeline
Fix wrong timespan on acceleration timeline
2024-10-09 17:52:51 +09:00
natsoni
c4004ba301 Clean up timeline code 2024-10-09 17:50:24 +09:00
natsoni
74b420c258 Use adjusted block time for difficulty and ETA calculation 2024-10-09 17:05:29 +09:00
natsoni
15b7e75b69 Fix wrong timespan in acc timeline 2024-10-09 16:33:19 +09:00
softsimon
70384d8d9f Merge pull request #5577 from mempool/natsoni/fix-premine-amount
Fix rune premine amount
2024-10-08 21:17:13 +09:00
natsoni
2a27ee0c7c Fix rune premine amount 2024-10-08 19:20:08 +09:00
nymkappa
933a204462 [accelerator] fee delta matches what the user accepted to pay in frontend 2024-10-08 18:37:03 +09:00
natsoni
6884830da6 Merge branch 'master' into natsoni/block-first-seen-audit 2024-10-08 15:04:46 +09:00
softsimon
24ec31acd9 Merge pull request #5569 from mempool/natsoni/ord
Add option to display runestones and inscriptions metadata
2024-10-08 13:21:43 +09:00
natsoni
1b2f1b38b4 undefined -> unknown 2024-10-08 13:09:19 +09:00
natsoni
3486c35f5e 50kb -> 100kb 2024-10-08 12:59:36 +09:00
natsoni
57a05c80a2 Move inscription type to utils 2024-10-08 12:53:18 +09:00
natsoni
1ddb8a39c9 Show text inscriptions up to 50kB 2024-10-08 12:50:56 +09:00
natsoni
0a61429176 Increase inscription max height 2024-10-08 12:41:14 +09:00
natsoni
e440c3f235 Fix edicts displaying 2024-10-08 12:40:25 +09:00
natsoni
177bbc83f3 Clean up etches fetching logic 2024-10-08 12:38:12 +09:00
Mononaut
040c067aac fix rune edict wrong id type bug 2024-10-08 02:49:46 +00:00
Mononaut
15b3c88a1f fix optional rune divisibility bug 2024-10-08 02:40:14 +00:00
natsoni
65f080d526 FIx error handling logic in ord-data 2024-10-08 11:24:17 +09:00
wiz
19347614bd Merge pull request #5514 from mempool/nymkappa/refactor-pool-subscription
[refactor] remove useless mining_pool subscriptions
2024-10-08 11:13:08 +09:00
softsimon
3b9601a82e Merge pull request #5575 from mempool/mononaut/minimal-runes
replace rune parsing libraries with minimal reimplementation
2024-10-08 11:10:54 +09:00
Mononaut
acae5a33b0 replace rune parsing dependencies with minimal reimplementation 2024-10-08 01:56:49 +00:00
natsoni
8b6db768cd Decode inscription / rune data client-side 2024-10-07 20:26:02 +09:00
natsoni
4143a5f593 Add runestone protocol implementation 2024-10-07 20:03:10 +09:00
natsoni
d31c2665ee Add inscriptions parsing code 2024-10-07 20:01:55 +09:00
softsimon
2142ae55d5 Merge pull request #5469 from mempool/nymkappa/configurable-pool-update
[mining] fix pools updater only running at start
2024-10-07 19:43:38 +09:00
softsimon
0c87a4e7f6 Merge branch 'master' into nymkappa/configurable-pool-update 2024-10-07 19:35:51 +09:00
softsimon
e6980a832b Merge branch 'master' into mononaut/use-acceleration-websocket 2024-10-07 15:58:13 +09:00
softsimon
b08b2ce44a Merge pull request #5574 from mempool/nymkappa/update-doc
update doc
2024-10-07 15:44:00 +09:00
nymkappa
d6b9e3118d [refactor] remove useless mining_pool subscriptions 2024-10-07 15:41:29 +09:00
nymkappa
9b4c93c8ee update doc 2024-10-07 15:35:52 +09:00
wiz
e59f5b8810 Merge pull request #5565 from mempool/nymkappa/update-doc
[accelerator] public accel history filter by miner unique id
2024-10-07 15:17:55 +09:00
softsimon
ddf1a300b6 Merge pull request #5573 from mempool/mononaut/fix-partial-utxo-chart
never show a utxo chart with missing data
2024-10-07 15:16:25 +09:00
Mononaut
8e223861d6 never show a utxo chart with missing data 2024-10-06 20:25:49 +00:00
softsimon
8808ff1a98 Merge pull request #5572 from mempool/dependabot/npm_and_yarn/frontend/rollup-4.24.0
Bump rollup from 4.13.0 to 4.24.0 in /frontend
2024-10-06 14:56:35 +09:00
dependabot[bot]
33a6ba04b6 Bump rollup from 4.13.0 to 4.24.0 in /frontend
Bumps [rollup](https://github.com/rollup/rollup) from 4.13.0 to 4.24.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.13.0...v4.24.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-06 05:46:06 +00:00
softsimon
d020858840 Merge pull request #5549 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.15.0
Bump cypress from 13.14.0 to 13.15.0 in /frontend
2024-10-06 14:44:50 +09:00
softsimon
5e0160a039 Merge pull request #5571 from mempool/natsoni/fix-block-page-loadig
Fix block page spinner loader
2024-10-06 13:40:22 +09:00
softsimon
2443bebae5 Merge pull request #5570 from mempool/natsoni/failed-canceled
Failed -> Canceled
2024-10-06 13:38:49 +09:00
natsoni
6fb68203bc Fix block page spinner loader 2024-10-06 11:50:18 +09:00
natsoni
d7acfad3d6 Failed -> Canceled in accelerations list 2024-10-06 11:23:42 +09:00
softsimon
a700bd0ef1 Merge pull request #5568 from mempool/natsoni/gigasats
fix gigasats -> billion sats
2024-10-05 04:43:47 -06:00
natsoni
ae2a849257 fix gigasats -> billion sats 2024-10-05 19:34:41 +09:00
natsoni
1a75e3e317 Store block first seen time in block audits 2024-10-05 19:26:33 +09:00
softsimon
ba167c9cc2 Merge pull request #5566 from mempool/natsoni/fix-block-health-display
Avoid briefly showing incorrect health value
2024-10-05 02:38:11 -06:00
natsoni
3d27b7e7b4 Avoid briefly showing incorrect health value 2024-10-05 17:16:00 +09:00
nymkappa
c4f73b80da [accelerator] public accel history filter by miner unique id 2024-10-05 16:19:19 +09:00
softsimon
76a1eb12a6 Merge pull request #5563 from mempool/natsoni/fix-block-health-display
Reset block audit on block navigation
2024-10-04 22:29:14 -06:00
natsoni
fe16f0dddc Reset block audit on block navigation 2024-10-05 13:19:05 +09:00
natsoni
67295c1b9b add debug.log path to backend config 2024-10-04 22:15:00 +09:00
wiz
0bd760d4d6 Merge pull request #5561 from mempool/nymkappa/fix-tests
fix tests
2024-10-03 16:11:07 +09:00
nymkappa
0f2340600c fix tests 2024-10-03 15:48:27 +09:00
softsimon
72c9d02f88 Merge pull request #5558 from mempool/mononaut/handle-utxo-error
handle /utxos error on address page
2024-10-01 12:01:55 -07:00
wiz
43a42d356d Enable RUST_GBT in backend by default 2024-10-02 04:00:33 +09:00
Mononaut
60adad8db3 handle /utxos error on address page 2024-09-29 09:30:09 +00:00
softsimon
5b73362e44 Merge pull request #5557 from mempool/mononaut/fix-effective-rate-bug
Don't clobber effective fee rates
2024-09-28 21:24:07 +04:00
Mononaut
517a37728c Don't clobber effective fee rates 2024-09-28 16:34:13 +00:00
softsimon
8876bb8f43 Merge pull request #5556 from mempool/simon/remove-rocket-beta
remove rocket beta
2024-09-28 10:50:15 +04:00
softsimon
da2341dd00 remove rocket beta 2024-09-28 08:56:29 +04:00
softsimon
146935efaf Merge pull request #5553 from mempool/mononaut/cors-expose-custom-header
expose custom x-total-count header
2024-09-28 01:39:37 +04:00
softsimon
775fcbab31 Merge pull request #5552 from mempool/nymkappa/satoshi-pipe-update
export bitcoinsatoshis pipe module, allow custom class for first part
2024-09-28 00:04:56 +04:00
softsimon
cb12e66a3b Merge pull request #5554 from mempool/mononaut/fix-accel-paging
fix acceleration history paging w/ undefined total
2024-09-28 00:02:55 +04:00
Mononaut
ea08c0c950 fix acceleration history paging w/ undefined total 2024-09-27 16:09:12 +00:00
Mononaut
b26d26b14c expose custom x-total-count header 2024-09-27 15:55:29 +00:00
nymkappa
2d7316942f export bitcoinsatoshis pipe module, allow custom class for first part 2024-09-27 17:26:27 +02:00
wiz
676abf58fd Merge pull request #5551 from mempool/mononaut/utxo-chart-navigation
fix utxo chart on-click navigation
2024-09-27 07:50:15 +09:00
Mononaut
1d5843a112 fix utxo chart on-click navigation 2024-09-26 22:14:44 +00:00
softsimon
9bfe1fb15e Merge pull request #5550 from mempool/mononaut/truncate-miner-name
refactor miner name truncation
2024-09-26 23:59:12 +04:00
Mononaut
b29c4cf228 refactor miner name truncation 2024-09-26 17:18:49 +00:00
softsimon
1f84e1722f Merge pull request #5539 from BitcoinMechanic/add-miner-name
Show miner name on block timeline
2024-09-26 21:11:18 +04:00
dependabot[bot]
2ad52e2c78 Bump cypress from 13.14.0 to 13.15.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.14.0 to 13.15.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.14.0...v13.15.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-26 02:37:00 +00:00
softsimon
758122db5e Merge pull request #5548 from mempool/mononaut/utxo-chart-optimization
utxo chart optimization
2024-09-25 11:50:36 +08:00
Mononaut
83b6094174 optimize utxo graph layout algorithm, enable transitions 2024-09-25 00:03:15 +00:00
Stephan Oeste
7057b31c3c Add testnet4 to install script 2024-09-24 20:04:33 +02:00
Mononaut
9091fc9210 add missing time.service.ts file 2024-09-24 15:55:23 +00:00
mononaut
d149c8bd24 Merge branch 'master' into mononaut/utxo-chart-colors 2024-09-24 09:39:06 -06:00
Mononaut
9984621e5e refactor static time formatting into new service 2024-09-24 15:37:55 +00:00
softsimon
54a27ef89f Merge pull request #5545 from mempool/natsoni/fix-negative-time
Don't show negative timespans on timeline
2024-09-24 17:07:43 +08:00
mononaut
81ddce27df Merge branch 'master' into add-miner-name 2024-09-23 16:07:20 -06:00
BitcoinMechanic
e6dbde952e Strip non-alphanumeric chars from miner names 2024-09-23 12:36:10 -07:00
Mononaut
be49f70b09 [accelerator] get acceleration updates over websocket 2024-09-23 17:17:28 +00:00
natsoni
2a9346f695 Don't show negative timespans on timeline 2024-09-23 14:47:57 +02:00
softsimon
05e88a25be npm audit fix 2024-09-23 14:15:00 +08:00
softsimon
574a800520 Merge pull request #5542 from mempool/dependabot/npm_and_yarn/frontend/esbuild-0.24.0
Bump esbuild from 0.23.0 to 0.24.0 in /frontend
2024-09-23 13:44:14 +08:00
softsimon
92de208414 Merge branch 'master' into mononaut/utxo-chart-colors 2024-09-23 13:01:32 +08:00
softsimon
1b4bbc24ba Merge pull request #5541 from mempool/mononaut/update-pool-pie
Update accelerating pie chart in real time
2024-09-23 12:57:07 +08:00
dependabot[bot]
0e5698955f Bump esbuild from 0.23.0 to 0.24.0 in /frontend
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-23 02:09:59 +00:00
BitcoinMechanic
4220f99477 remove 'on'/UI changes per feedback 2024-09-22 14:46:53 -07:00
Mononaut
e144e139b7 Update accelerating pie chart in real time 2024-09-22 18:06:55 +00:00
Mononaut
06e699e52b address utxo chart color by age & updates 2024-09-22 17:09:35 +00:00
softsimon
07a0850f99 Merge pull request #5437 from mempool/simon/use-sats
only use sats, not sat
2024-09-23 00:33:19 +08:00
softsimon
72a5f4a521 amount selector sat -> sats 2024-09-23 00:18:59 +08:00
softsimon
04605e10a5 only use sats, not sat 2024-09-23 00:15:12 +08:00
softsimon
407ce3c76d Merge pull request #5491 from mempool/nymkappa/accel-bidboost-graph
[accelerator] make bid boost graph bar min height taller
2024-09-22 23:48:41 +08:00
softsimon
a99278320b Merge pull request #5451 from mempool/mononaut/optimize-gbt-process-blocks
optimize processNewBlocks
2024-09-22 23:46:29 +08:00
softsimon
367ee68fe0 Merge branch 'master' into mononaut/optimize-gbt-process-blocks 2024-09-22 12:41:51 +08:00
softsimon
1038b4f908 Merge pull request #5538 from mempool/natsoni/fix-wrong-block-skeleton
Fix race condition between accelerations and block audit api calls
2024-09-22 12:39:19 +08:00
BitcoinMechanic
b90cd4c7e3 restore minerNames property on pool 2024-09-20 14:59:21 -07:00
BitcoinMechanic
25482b9a06 show miner name on block timeline 2024-09-20 14:31:31 -07:00
natsoni
e41829d5e0 Fix race condition between accelerations and block audit api calls 2024-09-20 16:24:33 +02:00
softsimon
156bf12034 Merge pull request #5521 from mempool/dependabot/npm_and_yarn/backend/multi-d66d039ac5
Bump serve-static and express in /backend
2024-09-20 16:12:48 +08:00
softsimon
fc1cdbac22 Merge pull request #5522 from mempool/dependabot/npm_and_yarn/frontend/multi-9423f4c335
Bump body-parser and express in /frontend
2024-09-20 16:05:32 +08:00
softsimon
5c839aced3 Merge pull request #5486 from mempool/natsoni/remove-da-offset
Remove difficulty adjustment block offset
2024-09-20 15:37:14 +08:00
softsimon
3345a60863 Merge pull request #5532 from mempool/natsoni/fix-accelerations-list
Fix accelerations list page navigation on first load
2024-09-20 01:11:40 +08:00
mononaut
36844f5b70 Merge pull request #5533 from mempool/natsoni/add-blocks-logo
Add logos to blocks and test transactions pages
2024-09-19 10:44:53 -06:00
mononaut
46d99db167 Merge branch 'master' into natsoni/add-blocks-logo 2024-09-19 10:44:38 -06:00
softsimon
76dcb0830a Merge pull request #5529 from mempool/natsoni/footer-tm
"Be your own explorer" on non official mempool instance
2024-09-19 23:13:41 +08:00
natsoni
0b29b61e93 "Be your own explorer" on non official mempool instance 2024-09-19 17:07:31 +02:00
softsimon
f4425ed7fe Merge pull request #5524 from mempool/simon/calculator-numeric-fix
Fix critical calculator inputmode
2024-09-19 22:53:24 +08:00
softsimon
7c02eab630 Merge pull request #5525 from mempool/mononaut/utxo-chart
Add utxo chart to address page
2024-09-19 22:51:49 +08:00
softsimon
8867ef9680 Merge pull request #5534 from mempool/natsoni/mining-1m
Only fetch 1m mining stats
2024-09-19 22:50:29 +08:00
softsimon
1048a0ea83 Merge pull request #5536 from mempool/natsoni/fix-timeline-tooltip-width
Wrap pool logos in timeline tooltip
2024-09-19 22:46:40 +08:00
softsimon
7b216f7ec7 Merge pull request #5535 from mempool/natsoni/fix-eta-error
Fix ETA calculation error
2024-09-19 22:41:25 +08:00
natsoni
32cc2f0c63 Blocks logo wraps before 'Test Transactions' text 2024-09-19 16:33:31 +02:00
natsoni
c6b0e5ff0e Acceleration timeline tooltip: wrap pool logos 2024-09-19 16:08:39 +02:00
natsoni
68a580466f Fix ETA calculation error 2024-09-19 15:17:24 +02:00
natsoni
bb06a66a03 Only fetch 1m mining stats 2024-09-19 14:59:34 +02:00
natsoni
ec6372464f Add logos to blocks and test transactions pages 2024-09-19 14:38:37 +02:00
softsimon
c13b8029d3 Merge pull request #5527 from mempool/natsoni/hide-eta-replaced-tx
Pizza tracker: don't show ETA on replaced tx
2024-09-19 19:10:35 +08:00
softsimon
88e92b1b34 Merge pull request #5513 from mempool/nymkappa/fix-double-click
[accelerator] avoid duplicated accel request with double click
2024-09-19 17:46:15 +08:00
softsimon
b0fa3efbbb Merge pull request #5526 from mempool/natsoni/fix-mobile-tx-push
Fix mobile routing to tx push and test pages
2024-09-19 17:44:33 +08:00
softsimon
0ccb5618f6 Merge pull request #5530 from mempool/mononaut/fix-acc-eta
Fix off-by-one error in multi-pool eta calculation
2024-09-19 01:28:43 +08:00
natsoni
556313a676 Fix accelerations list page navigation on fist load 2024-09-18 16:56:00 +02:00
softsimon
1fe08a9ecc Merge pull request #5528 from mempool/natsoni/tracker-support-error
Show http error in pizza tracker
2024-09-18 22:33:48 +08:00
softsimon
a11116ff3a Merge pull request #5531 from mempool/natsoni/accelerator-logo-width
Fix accelerator logo in trademark policy on mobile
2024-09-18 22:33:18 +08:00
natsoni
ede0ccfd2e Fix accelerator logo width in trademark policy 2024-09-18 15:38:36 +02:00
Mononaut
b64caf8f4b Fix off-by-one error in multi-pool eta calculation 2024-09-17 20:30:25 +00:00
natsoni
99290a7946 Show http error in pizza tracker 2024-09-17 15:08:03 +02:00
natsoni
2d9709a427 Pizza tracker: hide ETA on replaced tx 2024-09-17 14:56:25 +02:00
natsoni
a76d6c2949 Fix mobile routing to tx push and test pages 2024-09-17 14:50:44 +02:00
softsimon
d8cfc6e32d Merge pull request #5471 from mempool/natsoni/hide-acc-checkout-on-accelerations
Avoid brief display of accelerator checkout on already accelerated txs
2024-09-15 16:07:58 +08:00
softsimon
9457032ab1 Merge branch 'master' into natsoni/hide-acc-checkout-on-accelerations 2024-09-14 23:12:43 +08:00
softsimon
74998e7f56 Merge pull request #5482 from mempool/natsoni/hide-acc-panel-on-acceleration
Hide accelerator panel if tx gets accelerated on another session
2024-09-14 20:15:39 +08:00
softsimon
db0f968749 Merge pull request #5481 from mempool/natsoni/fix-acceleration-flow-state
Reset acceleration flow state when leaving transaction
2024-09-14 17:52:50 +08:00
Mononaut
a1968e01e5 Add utxo chart to address page 2024-09-13 18:16:29 +00:00
softsimon
c7ab6b03fb Fix critical calculator inputmode 2024-09-13 23:23:22 +08:00
softsimon
2b206a7bcc Merge branch 'master' into natsoni/hide-acc-checkout-on-accelerations 2024-09-13 23:02:23 +08:00
softsimon
c4b90c2a18 Merge pull request #5485 from mempool/mononaut/paginated-accel-history
Handle paginated acceleration results
2024-09-13 22:56:57 +08:00
softsimon
2a7d5760e0 Merge branch 'master' into mononaut/paginated-accel-history 2024-09-13 22:16:28 +08:00
softsimon
c8719f1f1e Merge pull request #5479 from mempool/mononaut/rbf-tracking-fixes
RBF tracking fixes
2024-09-13 22:11:27 +08:00
natsoni
d199c7746e Merge branch 'master' into natsoni/hide-acc-checkout-on-accelerations 2024-09-13 11:57:20 +02:00
dependabot[bot]
67eb815992 Bump body-parser and express in /frontend
Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `body-parser` from 1.20.2 to 1.20.3
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-13 09:10:42 +00:00
dependabot[bot]
4ccd3c8525 Bump serve-static and express in /backend
Bumps [serve-static](https://github.com/expressjs/serve-static) to 1.16.2 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `serve-static` from 1.15.0 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-13 09:10:33 +00:00
softsimon
fc5af24b68 Merge branch 'master' into mononaut/rbf-tracking-fixes 2024-09-13 17:10:12 +08:00
softsimon
b17b66a52f Merge pull request #5476 from mempool/dependabot/npm_and_yarn/frontend/micromatch-4.0.8
Bump micromatch from 4.0.4 to 4.0.8 in /frontend
2024-09-13 17:09:53 +08:00
softsimon
819dedbc88 Merge pull request #5464 from mempool/natsoni/amount-selector
Add amount mode selector to footer
2024-09-13 17:03:24 +08:00
softsimon
8717051a06 Merge pull request #5472 from mempool/mononaut/json-errors
respect json Accept header in API error responses
2024-09-13 16:13:48 +08:00
nymkappa
3e78b636d6 [accelerator] avoid duplicated accel request with double click 2024-09-12 16:02:11 +02:00
Mononaut
7865574bd4 block summary indexing respect INDEXING_BLOCKS_AMOUNT 2024-09-11 21:48:26 +00:00
nymkappa
b3ca8840e5 Merge branch 'master' into nymkappa/faucet-unverified 2024-09-11 16:51:22 +02:00
softsimon
75316e60d0 Merge branch 'master' into mononaut/json-errors 2024-09-10 18:37:27 +04:00
softsimon
31469ad361 Merge pull request #5478 from mempool/natsoni/ineligible-for-acceleration
Ineligible transaction link to accelerator FAQ
2024-09-10 18:36:19 +04:00
nymkappa
a133ddf062 [faucet] show unverified warning if no email provided 2024-09-10 12:07:46 +02:00
wiz
485a58e453 Merge pull request #5503 from mempool/mononaut/axios-socket-hotfix-2
hotfix option 2 for axios breaking change to unix sockets
2024-09-09 16:54:28 +09:00
wiz
92090399cc Merge pull request #5504 from mempool/revert-5502-mononaut/axios-socket-hotfix-1
Revert "hotfix option 1 for axios breaking change to unix sockets"
2024-09-09 16:54:12 +09:00
wiz
893c3cd87d Revert "hotfix option 1 for axios breaking change to unix sockets" 2024-09-09 16:53:56 +09:00
wiz
c93159414c Merge pull request #5502 from mempool/mononaut/axios-socket-hotfix-1
hotfix option 1 for axios breaking change to unix sockets
2024-09-09 16:42:23 +09:00
Mononaut
b2d4f4078f alternate hotfix for broken socket support (rollback axios to 1.7.2) 2024-09-08 20:18:04 +00:00
Mononaut
be17e45785 hotfix for axios breaking change to unix sockets 2024-09-08 20:16:06 +00:00
wiz
dbe774cc64 ops: Clear all mempool frontend configs on build env reset 2024-09-09 02:45:16 +09:00
Mononaut
e513f05c09 Fix reorg to lower height on /monitoring status page 2024-09-05 20:55:48 +00:00
wiz
64223c4744 ops: Set blocksxor=0 in bitcoin.conf 2024-09-05 02:15:08 +09:00
wiz
07fd3d3409 ops: Bump some FreeBSD install packages 2024-09-04 22:26:08 +09:00
wiz
f7360433a1 Merge pull request #5497 from mempool/nymkappa/nofaucet-error-message
[faucet] add missing error message for suspicious twitter accounts
2024-09-04 21:06:47 +09:00
nymkappa
f6fac92180 [faucet] add missing error message for suspicious twitter accounts 2024-09-04 13:52:48 +02:00
wiz
82d1502bfa Merge pull request #5487 from mempool/orangeusurf/about-enterprise-update
Update about page enterprise sponsors
2024-09-03 13:40:06 +09:00
orangesurf
8ab104d191 switch to alternate logo 2024-09-02 13:47:41 +02:00
orangesurf
263742132c Merge branch 'master' into orangeusurf/about-enterprise-update 2024-09-02 12:28:52 +01:00
softsimon
e3c3f31ddb Merge pull request #5494 from vostrnad/zero-multisig
Allow OP_0 in multisig scripts
2024-09-01 10:49:27 +04:00
wiz
70d1f52268 Merge pull request #5489 from mempool/mononaut/the-v3-standard 2024-09-01 02:02:21 +09:00
Vojtěch Strnad
e44f30d7a7 Allow OP_0 in multisig scripts 2024-08-31 14:31:55 +02:00
Mononaut
099d84a395 New standardness rules for v3 & anchor outputs, with activation height logic 2024-08-30 23:16:33 +00:00
Mononaut
12285465d9 Add support for anchor output type 2024-08-30 21:39:22 +00:00
natsoni
eab008c707 Ineligible transaction link to accelerator FAQ 2024-08-30 12:19:58 +02:00
nymkappa
0f1def5822 [accelerator] make bid boost graph bar min height taller 2024-08-29 20:53:40 +02:00
orangesurf
fad39e0bea Update about page enterprise sponsors 2024-08-29 13:02:32 +02:00
natsoni
0a5a2c3c7e Remove difficulty epoch block offset 2024-08-28 16:50:00 +02:00
Mononaut
b526ee0877 Handle paginated acceleration results 2024-08-28 14:47:42 +00:00
dependabot[bot]
98d98b2478 Bump micromatch from 4.0.4 to 4.0.8 in /frontend
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.4 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.4...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 05:01:52 +00:00
softsimon
3bea10ea35 Merge pull request #5484 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.14.0
Bump cypress from 13.13.0 to 13.14.0 in /frontend
2024-08-28 07:00:55 +02:00
dependabot[bot]
1ea45e9e96 Bump cypress from 13.13.0 to 13.14.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.13.0 to 13.14.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.13.0...v13.14.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 02:59:35 +00:00
natsoni
555425d97e Handle city-states in geolocation component 2024-08-27 14:49:54 +02:00
natsoni
624b3473fc Hide accelerator panel if tx gets accelerated on another session 2024-08-27 12:32:21 +02:00
natsoni
a3e61525fe Reset acceleration flow state when leaving transaction 2024-08-27 11:42:13 +02:00
Mononaut
9e05060af4 fix tests 2024-08-27 00:17:17 +00:00
Mononaut
ee53597fe9 Resume RBF trees after restart 2024-08-26 23:43:29 +00:00
Mononaut
e362003746 Catch RBF replacements across mempool update boundaries 2024-08-26 23:43:15 +00:00
Mononaut
185eae00e9 Fix RBF tracking inconsistencies 2024-08-26 23:41:38 +00:00
softsimon
8c2d0e1d6c Merge pull request #5445 from mempool/mononaut/persistent-goggles
Persist mempool block visualization between pages
2024-08-26 22:14:17 +02:00
softsimon
009fba3dd5 Merge branch 'master' into mononaut/persistent-goggles 2024-08-26 22:05:16 +02:00
softsimon
a0fc4861d4 Merge pull request #5460 from mempool/mononaut/v1-audit-improvements
v1 audit improvements
2024-08-26 21:38:37 +02:00
softsimon
62085581dd Merge branch 'master' into mononaut/v1-audit-improvements 2024-08-26 18:45:00 +02:00
softsimon
05efa8c300 Merge pull request #5468 from mempool/dependabot/npm_and_yarn/frontend/elliptic-6.5.7
Bump elliptic from 6.5.4 to 6.5.7 in /frontend
2024-08-26 17:59:21 +02:00
softsimon
eee99a6407 Merge pull request #5477 from mempool/mononaut/readme-node-version
[docs] update READMEs to newer node version
2024-08-26 17:58:52 +02:00
Mononaut
98cee4a6cd [docs] update READMEs to newer node version 2024-08-26 15:36:53 +00:00
dependabot[bot]
0302999806 Bump elliptic from 6.5.4 to 6.5.7 in /frontend
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.4 to 6.5.7.
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.4...v6.5.7)

---
updated-dependencies:
- dependency-name: elliptic
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 14:54:51 +00:00
softsimon
1876d67e74 Merge pull request #5467 from mempool/dependabot/npm_and_yarn/backend/axios-1.7.4
Bump axios from 1.7.2 to 1.7.4 in /backend
2024-08-26 16:53:40 +02:00
softsimon
c0bb75e5b1 Merge pull request #5475 from mempool/dependabot/npm_and_yarn/frontend/tslib-2.7.0
Bump tslib from 2.6.2 to 2.7.0 in /frontend
2024-08-26 16:53:26 +02:00
dependabot[bot]
4059a902a1 Bump tslib from 2.6.2 to 2.7.0 in /frontend
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.6.2 to 2.7.0.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/v2.6.2...v2.7.0)

---
updated-dependencies:
- dependency-name: tslib
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 12:42:46 +00:00
dependabot[bot]
4cc19a7235 Bump axios from 1.7.2 to 1.7.4 in /backend
Bumps [axios](https://github.com/axios/axios) from 1.7.2 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 12:42:42 +00:00
wiz
c874d642c5 Bump version to 3.1.0-dev 2024-08-26 21:41:26 +09:00
wiz
f0af1703da Release v3.0.0 2024-08-24 18:35:41 +09:00
Mononaut
b47e148677 respect json Accept header in API error responses 2024-08-22 19:51:28 +00:00
natsoni
d22743c4b8 Don't display accelerator checkout on already accelerated txs 2024-08-22 15:39:20 +02:00
nymkappa
6db4afe878 [mining] add POOLS_UPDATE_DELAY where needed 2024-08-20 14:31:07 +02:00
nymkappa
4596394100 [mining] pool updater is now self contained service 2024-08-20 12:07:20 +02:00
nymkappa
ae2ed8fdae [mining] fix pools updater only running at start 2024-08-20 11:53:48 +02:00
softsimon
5452d7f524 pull from transifex 2024-08-20 09:32:56 +02:00
wiz
ff9e2456b9 ops: Tweak build script to support tags 2024-08-20 15:22:16 +09:00
wiz
4e581347c8 Bump version to v3.0.0-rc1 2024-08-20 12:06:11 +09:00
wiz
820777236e Merge pull request #5465 from mempool/orangesurf/update-logos-2024-08-19
Add logo images and references to logos
2024-08-20 12:01:51 +09:00
wiz
beeb5eb08c Merge pull request #5466 from mempool/mononaut/fix-about-layout
Fix about page layout
2024-08-20 10:50:04 +09:00
Mononaut
b78aca0282 Fix about page layout 2024-08-19 19:46:22 +00:00
orangesurf
9572f2d554 Add logo images and references to logos 2024-08-19 20:13:49 +02:00
natsoni
e59308c2f5 Fix global footer css 2024-08-19 18:21:01 +02:00
softsimon
ef13596b59 Merge pull request #5449 from mempool/mononaut/non-acc-effective-fee
fix mined acceleration detection logic on tx pages
2024-08-19 17:10:46 +02:00
natsoni
c7f48b4390 Add amount mode selector to footer 2024-08-19 16:29:34 +02:00
wiz
80da024bbb Add hr locale to angular.json 2024-08-19 14:49:36 +09:00
natsoni
f75f85f914 Hide fee delta on accelerated tx mined by participating pool with 0 bid boost 2024-08-18 19:43:38 +02:00
natsoni
b3ac107b0b clear feeDelta if a tx is mined by non-participating pool 2024-08-18 18:35:30 +02:00
softsimon
f8cedaa7a3 Merge pull request #5462 from mempool/natsoni/fix-console-error
Fix accelerated arrow not appearing
2024-08-18 15:47:02 +02:00
natsoni
72bb92dd8b Merge branch 'master' into mononaut/non-acc-effective-fee 2024-08-18 14:27:48 +02:00
natsoni
e3c4e219f3 Fix accelerated arrow not appearing 2024-08-18 14:15:56 +02:00
wiz
aa3fa4478a Merge pull request #5458 from mempool/mononaut/pool-pie-colors
update pool pie chart color scheme
2024-08-18 12:15:27 +09:00
Mononaut
c9171224e1 DB migration to fix bad v1 audits 2024-08-18 02:21:12 +00:00
Mononaut
248cef7718 Improve prioritized transaction detection algorithm 2024-08-18 02:21:05 +00:00
Mononaut
26c03eee88 update pool pie chart color scheme 2024-08-14 14:21:47 +00:00
softsimon
db10ab9aae pull from transifex 2024-08-13 10:28:42 +02:00
wiz
2ee7b9531a Merge pull request #5454 from mempool/simon/add-croatian
Add Croatian language
2024-08-13 13:36:00 +09:00
wiz
5f6af83944 Merge pull request #5453 from mempool/mononaut/acceleration-sparkles
acceleration sparkles
2024-08-13 13:33:25 +09:00
softsimon
8d2204a53f Merge pull request #5457 from mempool/mononaut/flow-output-indices
flow diagram zero-indexed inputs & outputs
2024-08-12 23:04:34 +02:00
Mononaut
96bec279a9 flow diagram zero-indexed inputs & outputs 2024-08-12 14:54:51 +00:00
softsimon
5178ae43f6 Add Croatian language 2024-08-12 00:07:48 +02:00
softsimon
ca26154426 pull from transifex 2024-08-11 23:51:16 +02:00
Mononaut
021f0b32a1 sparklier sparkles 2024-08-11 20:52:26 +00:00
Mononaut
b8cfeb579b make accelerations magical again 2024-08-11 20:38:54 +00:00
softsimon
fc5b99f93f Merge pull request #5452 from mempool/mononaut/tx-v1-audit
Implement v1 audit in tx audit API
2024-08-11 00:18:09 +02:00
Mononaut
ce4b0ed0f3 Implement v1 audit in tx audit API 2024-08-10 21:57:31 +00:00
Mononaut
a31729b8b8 fix feeDelta display logic 2024-08-10 21:56:11 +00:00
Mononaut
fbf27560b3 optimize processNewBlocks 2024-08-10 13:53:49 +00:00
Mononaut
79e494150c fix mined acceleration detection logic on tx pages 2024-08-09 14:44:51 +00:00
softsimon
b1a43abc0e Merge pull request #5444 from mempool/natsoni/fix-pool-pie-position
Fix pool pie position on safari
2024-08-08 22:20:09 +02:00
softsimon
3e50a3c9e7 pull from transifex 2024-08-08 18:58:59 +02:00
Mononaut
104c7f4285 Persist mempool block visualization between pages 2024-08-08 13:12:31 +00:00
natsoni
132d6204c3 Fix pool pie position on safari 2024-08-08 11:23:56 +02:00
wiz
77c6ad5576 Merge pull request #5438 from mempool/natsoni/hide-fee-delta-on-confirmed
Hide fee delta on accelerated tx if bid boost is 0
2024-08-07 19:38:04 -04:00
wiz
4d35845c18 Merge pull request #5441 from mempool/simon/remove-testnet4-beta
remove testnet4 beta tag
2024-08-07 19:36:55 -04:00
wiz
3d8a4a85f7 Merge pull request #5433 from mempool/simon/remove-mempool-goggles-beta
remove mempool googles [beta] tag
2024-08-07 19:36:45 -04:00
wiz
1545347a45 Merge pull request #5443 from mempool/simon/fix-broken-sponsor-image-proxy
fix broken sponsor image proxy
2024-08-07 18:11:59 -04:00
softsimon
9facf28ba5 pull from transifex 2024-08-07 23:57:11 +02:00
softsimon
d6eb98561b Merge pull request #5439 from mempool/natsoni/purple-acc-fee-rate
Consistent purple accelerated fee rate
2024-08-07 23:42:37 +02:00
softsimon
0f688e8347 fix broken sponsor image proxy 2024-08-07 23:41:53 +02:00
wiz
7f53741a7b Merge pull request #5442 from mempool/simon/timesincepaid-sorry-time-increase
increasing time since paid sorry message
2024-08-07 17:19:11 -04:00
softsimon
d5672691e1 increasing time since paid sorry message 2024-08-07 22:06:22 +02:00
softsimon
e6049c707b remove testnet4 beta tag 2024-08-07 22:03:47 +02:00
natsoni
91e74e769c Purple accelerated fee rate everywhere 2024-08-07 14:19:59 +02:00
natsoni
7f252f06b7 Hide fee delta on accelerated tx with bidBoost=0 2024-08-07 11:11:31 +02:00
softsimon
1b9d3f669d remove mempool googles [beta] tag 2024-08-07 00:30:20 +02:00
wiz
ef0ba9a77a ops: Add mempool-update-repo script 2024-08-06 18:30:11 -04:00
wiz
15f10736e2 Merge pull request #5432 from mempool/simon/match-any-onion
match any onion
2024-08-06 18:26:31 -04:00
softsimon
924399df46 match any onion 2024-08-07 00:24:23 +02:00
wiz
cddff129b3 Merge pull request #5431 from mempool/simon/fix-services-onion
fix services onion url
2024-08-06 17:29:15 -04:00
softsimon
7c90e8ae06 fix services onion url 2024-08-06 23:27:59 +02:00
wiz
34d996c7cb Merge pull request #5430 from mempool/natsoni/dynamically-show-oob-fee
Immediately show oob fee on accelerated transaction
2024-08-06 15:00:49 -04:00
wiz
641a2ae3ae Remove testnet4 not-yet-finalized warning now that BIP is merged 2024-08-05 15:37:29 -04:00
natsoni
11a849ef28 Immediately show oob fee on accelerated transaction 2024-08-05 21:01:33 +02:00
wiz
7b0347e846 Bump version string to 3.0.0-beta 2024-08-05 14:55:59 -04:00
wiz
bb1352ed58 Merge pull request #5427 from mempool/mononaut/fix-other-liquid-migration
fix db migration 75 on liquid
2024-08-05 14:09:36 -04:00
wiz
fa6456b92c Merge pull request #5422 from mempool/natsoni/purple-accelerated-rate
Purple accelerated fee rate
2024-08-05 14:09:14 -04:00
wiz
bfea19238b Merge pull request #5420 from mempool/natsoni/add-tooltip-acc-fee
Add tooltip to acceleration fee
2024-08-05 14:04:42 -04:00
wiz
36b91cfdfd Merge pull request #5421 from mempool/mononaut/rbf-tracker-redirect
Fix broken pizza rbf link to /tracker
2024-08-05 14:04:20 -04:00
wiz
7b56212064 Merge pull request #5425 from mempool/natsoni/clear-mining-cache-network-change
Clear mining service cache on network change
2024-08-05 14:03:14 -04:00
wiz
d4be3c2c4c Merge pull request #5428 from mempool/mononaut/payment-method-click
[accelerator] fix click binding on payment method buttons
2024-08-05 14:02:51 -04:00
wiz
3a9f06f651 Merge pull request #5429 from mempool/mononaut/fix-block-unfurl-loading
Fix stray loading spinner in block unfurl
2024-08-05 14:02:34 -04:00
Mononaut
e652eb339d Fix stray loading spinner in block unfurl 2024-08-05 17:08:44 +00:00
softsimon
96435c329f Merge pull request #5426 from mempool/mononaut/fix-liquid-migration
fix liquid db migration
2024-08-05 18:47:26 +02:00
Mononaut
1c69613d65 [accelerator] fix click binding on payment method buttons 2024-08-05 16:15:53 +00:00
Mononaut
3707763e30 fix db migration 75 on liquid 2024-08-05 16:03:22 +00:00
Mononaut
b6ce8229f0 fix liquid db migration 2024-08-05 15:49:16 +00:00
natsoni
b62ae9b6f6 Clear mining service cache on network change 2024-08-05 15:45:39 +02:00
Mononaut
f61ace2f92 Fix broken pizza rbf link to /tracker 2024-08-05 10:31:01 +00:00
natsoni
2b572f2494 Purple accelerated fee rate 2024-08-05 12:20:49 +02:00
natsoni
c0e4c1efe1 Fix text wrap 2024-08-05 11:53:01 +02:00
natsoni
8078caaa89 Add tooltip to acceleration fee 2024-08-05 11:36:35 +02:00
wiz
5eb117165f Revert "ops: Remove potentially dangerous env var in rust build process"
This reverts commit a2dcf0d545.
2024-08-04 21:19:44 -04:00
wiz
a2dcf0d545 ops: Remove potentially dangerous env var in rust build process 2024-08-04 21:06:36 -04:00
wiz
212d58f917 Change accelerate checkout redirect from /tracker/ to /tx/ 2024-08-04 20:52:49 -04:00
wiz
d1eec80afb Delete redirect to /tracker/ for cash.app 2024-08-04 20:45:05 -04:00
wiz
05c6709926 Merge pull request #5417 from mempool/simon/hide-fiat-buttons-correctly
hide fiat buttons correctly
2024-08-04 19:45:07 -04:00
softsimon
f1a48db9ee hide fiat buttons correctly 2024-08-05 01:43:58 +02:00
wiz
76ce43d289 Merge pull request #5416 from mempool/simon/enable-acc-button-enterprise
enable accelerator button on enterprise instances
2024-08-04 19:22:33 -04:00
softsimon
51f5b728f3 enable accelerator button on enterprise instances 2024-08-05 01:20:40 +02:00
wiz
8fa1863aff Merge pull request #5415 from mempool/simon/fix-invalid-json-response-requestAcceleration
fix invalid json response from requestAcceleration
2024-08-04 19:12:06 -04:00
softsimon
e3d1d9c1c0 fix invalid json response from requestAcceleration 2024-08-05 01:11:18 +02:00
wiz
f2f8d91e10 Merge pull request #5410 from mempool/natsoni/show-oob-fee
Show oob fees on tx details
2024-08-04 19:02:20 -04:00
wiz
11ef090846 Merge pull request #5414 from mempool/simon/only-enable-fiat-prod-staging
only enable fiat on prod and staging
2024-08-04 19:01:54 -04:00
softsimon
5cacd2635e only enable fiat on prod, staging and dev 2024-08-05 01:00:58 +02:00
softsimon
1b4780c25b Merge pull request #5409 from mempool/natsoni/fix-truncated-link
Fix truncated link to not refresh full window
2024-08-05 00:16:15 +02:00
softsimon
5ea44f2e7d Merge pull request #5408 from mempool/natsoni/pizza-tracker-fix-crash
Pizza tracker: handle transaction not yet in mempool
2024-08-04 23:39:39 +02:00
softsimon
261c794817 pull from transifex 2024-08-04 23:36:03 +02:00
natsoni
de9fae5cd7 Show oob fees on tx details 2024-08-04 17:07:38 +02:00
natsoni
439c52af30 Fix truncated link to not refresh full window 2024-08-04 14:36:42 +02:00
softsimon
4dbcd6ca18 pull from transifex 2024-08-04 10:18:18 +02:00
softsimon
2921c94520 Merge pull request #5354 from mempool/mononaut/v1-audits
v1 audits
2024-08-03 21:55:19 +02:00
softsimon
ab4a258be3 Merge pull request #5406 from mempool/simon/fix-mobile-rbf-test
fix mobile rbf test
2024-08-03 19:07:49 +02:00
softsimon
21b0d50947 fix mobile rbf test 2024-08-03 18:58:55 +02:00
softsimon
233ec112c2 Merge pull request #5405 from TechMiX/fix/more-accel-rtl-issues
fix: acceleration rtl layout issues
2024-08-03 18:24:22 +02:00
TechMiX
6f0a5c9b44 fix: accelerationn rtl layout issues 2024-08-03 14:39:50 +02:00
softsimon
2bc243b115 accel list i18n fixes 2024-08-03 00:26:37 +02:00
softsimon
154f8e65a7 accel status i18n fix 2024-08-03 00:20:38 +02:00
wiz
cc9855aa65 Merge pull request #5404 from mempool/nymkappa/square-loading
[square] use mirror to serve square.js and load it sooner
2024-08-02 17:52:44 -04:00
nymkappa
7b45d922bc [square] i'm an idiot 2024-08-02 23:35:30 +02:00
nymkappa
9488ca50a3 [square] use mirror to serve square.js and load it sooner 2024-08-02 23:27:40 +02:00
wiz
9f559248cc Merge pull request #5403 from mempool/nymkappa/square-loading
[square] retry web sdk faster
2024-08-02 17:24:00 -04:00
nymkappa
067aac4d06 [square] retry web sdk faster 2024-08-02 23:04:15 +02:00
wiz
37eb17cc22 Merge pull request #5402 from mempool/nymkappa/square-loading
[square] fix web sdk retry logic
2024-08-02 16:50:41 -04:00
nymkappa
c7382a1c6c [square] fix web sdk retry logic 2024-08-02 22:48:04 +02:00
softsimon
f782693b26 Merge pull request #5401 from mempool/mononaut/acc-pie-colors
adjust pie colors to handle more pools
2024-08-02 21:40:36 +02:00
wiz
908988d06e Merge pull request #5398 from mempool/natsoni/fix-accelerator-loader
Fix accelerator skeleton loader
2024-08-02 14:41:29 -04:00
wiz
a0c21e4120 Merge pull request #5395 from mempool/nymkappa/accel-status
[accelerator] fix accel status in widget
2024-08-02 14:25:54 -04:00
wiz
bd96cbd701 Merge pull request #5399 from mempool/natsoni/fix-mobile-tx-routing
Remove query param redirect on tx page
2024-08-02 14:23:54 -04:00
wiz
9cccdcf70f Merge pull request #5400 from mempool/natsoni/pizza-tracker-loader
Align pizza tracker skeleton loader
2024-08-02 14:23:21 -04:00
natsoni
2489fc4902 Fix need to double click on previous to go back 2024-08-02 19:00:52 +02:00
natsoni
e378df4158 Fix pizza tracker crash on unseen transaction 2024-08-02 18:52:07 +02:00
Mononaut
fa5f758875 adjust pie colors to handle more pools 2024-08-02 16:44:36 +00:00
natsoni
a46f15e7ec Align pizza tracker skeleton loader 2024-08-02 17:37:53 +02:00
natsoni
46ecd2a51f Fix accelerator skeleton loader 2024-08-02 16:53:34 +02:00
softsimon
c1aaf2f61a pull from transifex 2024-08-02 14:04:49 +02:00
Mononaut
d2b57c8d4f added/prioritized dual audit status 2024-08-02 10:46:07 +00:00
softsimon
f27a5700fa pull from transifex 2024-08-02 02:43:34 +02:00
nymkappa
17522cca6e [accelerator] 2024-08-02 00:24:50 +02:00
nymkappa
3f2581886d [accelerator] fix accel status in widget 2024-08-02 00:20:34 +02:00
wiz
c5beee3a40 ops: Fix nginx routing for /api/v1/accelerations 2024-08-01 17:04:00 -04:00
wiz
94e223e8da Merge pull request #5010 from mempool/mononaut/tracker-tx-routing
Transparently redirect direct mobile tx visits to pizza tracker
2024-08-01 14:30:56 -04:00
wiz
416e1bb4cb Merge pull request #5393 from mempool/mononaut/accelerate-anchor
[accelerator] add anchor element
2024-08-01 14:19:22 -04:00
wiz
564dc08509 Merge pull request #5392 from mempool/orangesurf/privacy-policy
Update PP
2024-08-01 14:19:01 -04:00
Mononaut
b9334c93f5 Handle accelerations in v1 audit 2024-08-01 14:07:19 +00:00
Mononaut
67761230e3 frontend support for v1 block audits 2024-08-01 14:07:19 +00:00
Mononaut
7cc01af631 Implement v1 block audits 2024-08-01 14:07:19 +00:00
Mononaut
96e2e6060b Migrate audits from v0 to v1 2024-08-01 14:07:19 +00:00
Mononaut
0723778e7c Update audit schema version 2024-08-01 14:07:19 +00:00
orangesurf
39d7e4ac3e Merge branch 'master' into orangesurf/privacy-policy 2024-08-01 13:44:34 +02:00
softsimon
1869368a49 pull from transifex 2024-08-01 10:50:01 +02:00
orangesurf
b83f19f186 Merge branch 'master' into orangesurf/privacy-policy 2024-07-31 23:50:58 +02:00
Mononaut
b248aad6d9 [accelerator] add anchor element 2024-07-31 17:08:39 +00:00
softsimon
87c9f920c1 Merge pull request #5391 from mempool/natsoni/add-opacity-pools-logo
Set pool logos opacity to 0.3
2024-07-31 18:34:09 +02:00
orangesurf
ea0e339d60 Update PP 2024-07-31 16:02:26 +02:00
orangesurf
0a0b5e52fe Update PP 2024-07-31 13:55:21 +02:00
natsoni
5314f35974 Make logos 0.3 opacity 2024-07-31 12:22:49 +02:00
softsimon
87d2f6cf90 Merge pull request #5389 from TechMiX/fix/acc-rtl-issues
fix: acceleration rtl issues
2024-07-31 11:16:29 +02:00
softsimon
4266bdcbb6 pull from transifex 2024-07-31 09:29:55 +02:00
softsimon
73e68534f1 Merge pull request #5390 from mempool/dependabot/npm_and_yarn/backend/babel/core-7.25.2
Bump @babel/core from 7.24.0 to 7.25.2 in /backend
2024-07-31 02:56:37 -04:00
dependabot[bot]
ee5b383a46 Bump @babel/core from 7.24.0 to 7.25.2 in /backend
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.24.0 to 7.25.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.2/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-31 02:27:25 +00:00
TechMiX
fa4b445dca fix acc rtl issues 2024-07-31 01:28:31 +02:00
softsimon
06eaadd517 pull from transifex 2024-07-30 16:17:21 -04:00
softsimon
ebdc1dbf6d Merge pull request #5385 from mempool/mononaut/fix-ffee-detection
Fix effective fee rate detection on tx page
2024-07-30 16:13:21 -04:00
softsimon
184903670a Merge pull request #5386 from mempool/natsoni/fix-timeline-tooltip
Fix missing accelerated fee rate in timeline tooltip
2024-07-30 14:49:06 -05:00
softsimon
878561244a Merge pull request #5387 from mempool/natsoni/fix-timeline-centering
Fix accelerated time centering
2024-07-30 14:48:19 -05:00
softsimon
a0e9b33199 pull from i18n 2024-07-30 15:46:29 -04:00
natsoni
0a4513c8fb Fix 'Just now' capitalization in timeline of mined transactions 2024-07-30 17:40:58 +02:00
softsimon
77b9277da5 pull from i18n 2024-07-30 10:38:05 -05:00
natsoni
009fac183f Fix accelerated time centering 2024-07-30 17:31:34 +02:00
natsoni
83a6df4d04 Fix missing accelerated fee rate line 2024-07-30 17:22:57 +02:00
Mononaut
474d384b0c fix wildcard routing clash 2024-07-30 14:48:47 +00:00
Mononaut
5870782abf Fix effective fee rate detection on tx page 2024-07-30 11:49:52 +00:00
softsimon
fb335f62db pull from i18n 2024-07-29 22:16:15 -05:00
softsimon
346ef0028d Merge pull request #5384 from mempool/dependabot/npm_and_yarn/backend/redis-4.7.0
Bump redis from 4.6.6 to 4.7.0 in /backend
2024-07-29 21:21:43 -05:00
dependabot[bot]
916cae2a8f Bump redis from 4.6.6 to 4.7.0 in /backend
Bumps [redis](https://github.com/redis/node-redis) from 4.6.6 to 4.7.0.
- [Release notes](https://github.com/redis/node-redis/releases)
- [Changelog](https://github.com/redis/node-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/node-redis/compare/redis@4.6.6...redis@4.7.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-30 02:12:44 +00:00
softsimon
550e667a2f Merge pull request #5383 from mempool/nymkappa/accel-history-list-update
[accelerator] use "mined" when accelerated tx is mined by non participating pool
2024-07-29 11:33:34 -05:00
softsimon
eb4ebf6786 pull from transifex 2024-07-29 11:09:12 -05:00
softsimon
7515348997 Merge pull request #5381 from TechMiX/hotfix/rtl-layout
fix: various rtl issues
2024-07-29 10:39:59 -05:00
nymkappa
41441bb958 [accelerator] use "mined" when accelerated tx is mined by non participating pool 2024-07-29 11:11:29 +02:00
softsimon
98c49918f7 Merge pull request #5382 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.11.0
Bump mysql2 from 3.10.0 to 3.11.0 in /backend
2024-07-28 21:14:52 -05:00
dependabot[bot]
5855e09663 Bump mysql2 from 3.10.0 to 3.11.0 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.10.0 to 3.11.0.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.10.0...v3.11.0)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-29 02:07:00 +00:00
softsimon
558b876d47 pull from transifex 2024-07-28 19:48:15 -05:00
TechMiX
fb63af5070 fix various rtl issues 2024-07-29 02:17:09 +02:00
wiz
d02a2ccf65 Merge pull request #5380 from mempool/simon/1w-1m
fixes crash with missing pool partner
2024-07-28 14:40:50 -05:00
softsimon
e3bb812203 fixes crash with missing pool partner
fixes #5379
2024-07-28 14:10:50 -05:00
softsimon
e3af4ea61c pull from transifex 2024-07-28 13:20:58 -05:00
softsimon
7d10b32861 pull from transifex 2024-07-26 16:43:17 -05:00
wiz
adea897e93 Merge branch 'master' into mononaut/tracker-tx-routing 2024-07-26 11:29:31 -05:00
wiz
17ec50dba0 Merge pull request #5371 from mempool/natsoni/timeline-tooltip
Add tooltip to acceleration timeline
2024-07-26 11:12:15 -05:00
Mononaut
53a36d042f Fix /tx redirect merge conflicts 2024-07-26 16:11:59 +00:00
softsimon
7531a53b2e correct i18n 2024-07-26 11:06:36 -05:00
Mononaut
d59bc085e5 Redirect direct mobile tx visits to pizza tracker 2024-07-26 15:54:22 +00:00
wiz
5d37e08c64 Merge branch 'master' into natsoni/timeline-tooltip 2024-07-26 10:44:01 -05:00
wiz
b5d89b83fa Merge pull request #5368 from mempool/nymkappa/google-pay
[accelerator] add support for Google Pay payment
2024-07-26 10:43:50 -05:00
nymkappa
1245673575 [accelerator] polish UI 2024-07-26 17:39:58 +02:00
softsimon
62a72755c7 pull from transifex 2024-07-26 10:25:29 -05:00
wiz
5b8ec8925a Merge pull request #5373 from mempool/mononaut/no-405
[accelerator] remove dumb log request 405 response
2024-07-26 10:22:38 -05:00
softsimon
262866c15b Merge pull request #5369 from mempool/nymkappa/getjwtemptynonofficial
[services] getJWT call returns nothing if non official
2024-07-26 06:36:21 -05:00
softsimon
aec7eb57c2 Merge pull request #5370 from mempool/natsoni/fix-loading-spinner
Fix loading spinner z-index
2024-07-26 05:10:25 -05:00
Mononaut
8d3b4733f5 [accelerator] remove dumb 405 log request response 2024-07-26 09:49:42 +00:00
nymkappa
3e4debdf7a Merge branch 'master' into nymkappa/google-pay 2024-07-26 11:38:47 +02:00
nymkappa
b719b76999 Merge pull request #5372 from mempool/mononaut/google-play-minor-fixes
minor accelerator checkout fixes
2024-07-26 11:37:54 +02:00
Mononaut
6adbda5185 Fix accelerator checkout linting & type errors 2024-07-26 09:27:18 +00:00
natsoni
01311d0ba1 Add tooltip to timeline 2024-07-26 11:21:24 +02:00
Mononaut
6081daacef [accelerator] add missing getters etc 2024-07-26 09:06:47 +00:00
natsoni
54c9970386 Merge branch 'master' into natsoni/timeline-tooltip 2024-07-26 11:04:17 +02:00
softsimon
1e4a599055 fix accelerations row height 2024-07-25 21:05:32 -05:00
softsimon
b051861d61 Merge pull request #5365 from mempool/natsoni/fix-mining-data
Get all pools in accelerations list
2024-07-25 21:03:52 -05:00
natsoni
3c7deafffd Fix loading spinner z-index 2024-07-26 00:00:14 +02:00
nymkappa
845123fc63 [services] getJWT call returns nothing if non official 2024-07-25 23:04:53 +02:00
nymkappa
d3e3650cac [accelerator] avoid premature square setup call 2024-07-25 21:49:48 +02:00
softsimon
008cc385da pull from transifex 2024-07-25 13:01:37 -05:00
softsimon
817a6bef6e Merge pull request #5367 from mempool/knorrium/change_default_var
Set the default value to the SERVICES_API variable
2024-07-25 12:59:40 -05:00
Felipe Knorr Kuhn
8dd8fe5fb1 Fix string value 2024-07-25 12:57:36 -05:00
softsimon
c734a81f08 pull from transifex 2024-07-25 12:27:32 -05:00
Felipe Knorr Kuhn
a0f4c260ab Set the default URL for the backend 2024-07-25 12:20:01 -05:00
Felipe Knorr Kuhn
000a989055 Set the default value to the SERVICES_API variable 2024-07-25 11:29:17 -05:00
natsoni
90331e2c1b Get all pools in accelerations list 2024-07-25 17:45:08 +02:00
wiz
78ac0137b3 Merge pull request #5350 from mempool/orangesurf/2024-07-19
Update webserver line
2024-07-25 10:21:53 -05:00
natsoni
3d9133c47e Add fee delta to acceleration data 2024-07-25 16:52:10 +02:00
nymkappa
481859bc8f [accelerator] add support for Google Pay payment 2024-07-25 15:54:24 +02:00
orangesurf
811feec145 Merge branch 'master' into orangesurf/2024-07-19 2024-07-25 18:47:27 +09:00
softsimon
b3e59c06e9 add new accelerator button config to sample 2024-07-25 04:07:18 -05:00
softsimon
67d44e3d6f Merge pull request #5359 from mempool/natsoni/fix-loop-calling-transactionTimes
Prevent never ending loop of calls to transactionTimes
2024-07-25 03:38:06 -05:00
softsimon
ca4b1943a8 moving code block 2024-07-25 03:35:17 -05:00
natsoni
df0f244bd1 Prevent never ending loop of calls to transactionTimes 2024-07-25 00:53:13 +02:00
softsimon
fe1ad86885 Merge pull request #5362 from mempool/natsoni/more-data-acc-table
Add fee delta and pool name to acceleration list
2024-07-24 17:42:28 -05:00
natsoni
aee2454a98 Add pool name to acceleration list 2024-07-25 00:24:08 +02:00
softsimon
5c814d9c22 pending balance -> pending 2024-07-24 17:21:55 -05:00
wiz
2c5d7fbc9f Merge pull request #5363 from mempool/nymkappa/apple-pay-hotfix
[accelerator] hide fiat payment method section if none available
2024-07-24 17:11:01 -05:00
nymkappa
570f7841ce [accelerator] hide fiat payment method section if none available 2024-07-25 00:10:27 +02:00
wiz
117c066425 Merge pull request #5353 from mempool/nymkappa/apple-pay
[accelerator] add support for acceleration with apple pay
2024-07-24 17:01:23 -05:00
softsimon
96f9f66e7f changing unconfirmed to pending balance/utxo 2024-07-24 16:56:30 -05:00
wiz
ce2742ff9c Merge branch 'master' into nymkappa/apple-pay 2024-07-24 16:47:57 -05:00
softsimon
4547a2757c Merge pull request #5351 from mempool/natsoni/fix-recursion-search
Fix recursion loop in search bar
2024-07-24 15:42:51 -05:00
nymkappa
4d44ee55fc [accelerator] add missing getters for applepay 2024-07-24 22:20:52 +02:00
nymkappa
29875e0095 Merge branch 'master' into nymkappa/apple-pay 2024-07-24 22:04:44 +02:00
softsimon
bc498733fc Merge pull request #5355 from mempool/natsoni/fix-blockchain-scroll
Fix unwanted blockchain scroll on screen resize
2024-07-24 11:44:36 -07:00
wiz
dc09e75783 Merge pull request #5361 from mempool/mononaut/fosscelerator
On-demand acceleration polling
2024-07-24 13:44:04 -05:00
wiz
544261eafe ops: Add /api/v1/accelerations to nginx hot cache 2024-07-24 13:31:56 -05:00
softsimon
58c0c060d5 Merge pull request #5358 from mempool/natsoni/fix-miner-loading
Fix miner loading forever
2024-07-24 11:25:48 -07:00
Mononaut
af7a962a0b [accelerator] accelerator_button config 2024-07-24 17:32:44 +00:00
Mononaut
7b3cc6372b [accelerator] frontend on-demand polling support 2024-07-24 17:32:44 +00:00
Mononaut
b49a6c4cac [accelerator] on-demand polling support 2024-07-24 17:32:43 +00:00
wiz
b0db348605 Merge pull request #5357 from mempool/nymkappa/update-api-key-header
[doc] update api key header
2024-07-24 12:27:03 -05:00
wiz
b1aa4f50bd Change X-Mempool-Authorization to X-Mempool-Auth 2024-07-24 12:25:35 -05:00
orangesurf
301f1821ae Merge branch 'master' into orangesurf/2024-07-19 2024-07-23 20:48:03 +09:00
natsoni
b9a053387f Add txConfirmed subscription variable to fix miner loading forever 2024-07-23 11:44:50 +02:00
softsimon
82c271267a extracting i18n's 2024-07-23 00:28:32 +08:00
softsimon
06affa60cc Merge pull request #5352 from mempool/simon/replace-crypto-uuid-func
Fix crypto lib call crash with custom function
2024-07-22 22:38:05 +08:00
nymkappa
4ef4e5b98a [doc] update api key header 2024-07-22 15:17:56 +02:00
wiz
6bc52dcc82 Merge pull request #5356 from mempool/simon/fix-payment-waiting-retry
fix btc payment waiting reply
2024-07-22 21:16:00 +09:00
softsimon
1f357408ac fix btc payment waiting reply 2024-07-22 20:13:13 +08:00
natsoni
a7be59df3e Fix buggy blockchain scroll on resize 2024-07-22 13:36:36 +02:00
orangesurf
82360f5525 Merge branch 'master' into orangesurf/2024-07-19 2024-07-22 17:08:47 +09:00
nymkappa
8762ccaa09 [accelerator] remove attempt to align fiat payment methods 2024-07-21 23:22:11 +02:00
nymkappa
3f7a24fb52 [accelerator] only show apple pay if available 2024-07-21 23:08:08 +02:00
nymkappa
09b09710e4 [accelerator] fix cashapp acceleration on mobile 2024-07-21 23:07:55 +02:00
nymkappa
08d3beed72 [accelerator] on mobile, autoscroll after selection cashapp or applepay 2024-07-21 22:38:49 +02:00
nymkappa
920f225e6c [accelerator] add support for acceleration with apple pay 2024-07-21 22:17:47 +02:00
softsimon
9c2d010516 rename to insecureRandomUUID 2024-07-21 23:55:28 +08:00
softsimon
743c7e8bfb Fix crypto lib call crash with custom function 2024-07-21 22:54:58 +08:00
softsimon
8116b50d50 fix spelling error 2024-07-21 20:34:20 +08:00
softsimon
df977926e2 Merge pull request #5349 from mempool/simon/add-fee-to-cpfp
Add fee to Cpfp API
2024-07-21 19:05:55 +08:00
softsimon
b0fac806d0 Merge pull request #4905 from mempool/mononaut/mini-miner-cpfp
Mini miner cpfp
2024-07-21 19:05:35 +08:00
Mononaut
398593828f Implement CPFP reindexing using mini-miner method (not activated) 2024-07-20 12:08:52 +00:00
Mononaut
9aac0ddce7 Fix merge conflict 2024-07-20 12:08:52 +00:00
Mononaut
79eb9635c2 Fix cpfp vsize rounding & goggles bugs 2024-07-20 12:08:52 +00:00
Mononaut
41c373c39d Mini-miner based block cpfp calculations 2024-07-20 12:08:52 +00:00
Mononaut
27374bd131 Refactor cpfp & single-block gbt code into mini-miner module 2024-07-20 12:08:52 +00:00
natsoni
8c07e3c31a Fix recursion loop in search bar 2024-07-19 16:12:54 +02:00
orangesurf
84ba721407 Update webserver line 2024-07-19 11:48:47 +02:00
softsimon
cdaf42797f Add fee to Cpfp API 2024-07-18 16:44:00 +08:00
wiz
0a116804e8 Merge pull request #5347 from mempool/junderw/fix-docker-aarch64
Bump Rust version to 1.79
2024-07-18 15:08:22 +09:00
junderw
68edf4306c Bump Rust version to 1.79
Maintaining an old MSRV is not a priority for this project.
If you would like to keep an old MSRV active, please maintain your own patch/fork.
2024-07-18 01:07:05 +09:00
softsimon
61bbb95819 fix default docker unix socket path variable 2024-07-17 12:34:28 +08:00
softsimon
3fa32edf25 Merge pull request #5345 from mempool/dependabot/npm_and_yarn/frontend/fortawesome/fontawesome-common-types-6.6.0
Bump @fortawesome/fontawesome-common-types from 6.5.1 to 6.6.0 in /frontend
2024-07-17 11:48:05 +09:00
dependabot[bot]
db220d9dfd Bump @fortawesome/fontawesome-common-types in /frontend
Bumps [@fortawesome/fontawesome-common-types](https://github.com/FortAwesome/Font-Awesome) from 6.5.1 to 6.6.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.5.1...6.6.0)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-common-types"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-17 02:47:44 +00:00
softsimon
bf61557879 Merge pull request #5344 from mempool/dependabot/npm_and_yarn/frontend/fortawesome/free-solid-svg-icons-6.6.0
Bump @fortawesome/free-solid-svg-icons from 6.5.1 to 6.6.0 in /frontend
2024-07-17 11:47:15 +09:00
dependabot[bot]
ebaf5cd304 Bump @fortawesome/free-solid-svg-icons from 6.5.1 to 6.6.0 in /frontend
Bumps [@fortawesome/free-solid-svg-icons](https://github.com/FortAwesome/Font-Awesome) from 6.5.1 to 6.6.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.5.1...6.6.0)

---
updated-dependencies:
- dependency-name: "@fortawesome/free-solid-svg-icons"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-17 02:47:02 +00:00
softsimon
51154d3954 Merge pull request #5343 from mempool/dependabot/npm_and_yarn/frontend/fortawesome/fontawesome-svg-core-6.6.0
Bump @fortawesome/fontawesome-svg-core from 6.5.1 to 6.6.0 in /frontend
2024-07-17 11:46:09 +09:00
dependabot[bot]
41b4b2eddf Bump @fortawesome/fontawesome-svg-core from 6.5.1 to 6.6.0 in /frontend
Bumps [@fortawesome/fontawesome-svg-core](https://github.com/FortAwesome/Font-Awesome) from 6.5.1 to 6.6.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.5.1...6.6.0)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-svg-core"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-17 02:34:33 +00:00
softsimon
0dff7e82a3 Merge pull request #5341 from mempool/simon/retry-firstseen-onerror
retry firstseen on error
2024-07-15 02:03:20 +09:00
softsimon
9cba7ccf75 retry firstseen on error
fixes #5340
2024-07-14 20:44:34 +09:00
wiz
6177b97bd1 Merge pull request #5337 from mempool/nymkappa/invoice-check-http-code
[btcpay] handle new http code 204 when calling /payments/bitcoin/check
2024-07-14 20:12:23 +09:00
softsimon
f7f1a99486 Merge pull request #5339 from mempool/mononaut/fix-pizza-status
fix pizza status
2024-07-14 17:22:53 +09:00
Mononaut
6b955acf9e [pizza] fix status icon layout w/ accelerator modal 2024-07-14 04:40:27 +00:00
Mononaut
530610add6 [pizza] fix trackerStage clobbered by ETA change 2024-07-14 04:39:46 +00:00
wiz
428f9369e2 Merge pull request #5338 from jlopp/jloppAgreement 2024-07-14 05:17:02 +09:00
softsimon
a9defb21bb restore timeline lowercase time 2024-07-14 02:06:34 +09:00
nymkappa
680e9562a0 [btcpay] handle new http code 204 when calling /payments/bitcoin/check api 2024-07-13 21:07:13 +09:00
wiz
66c5c303b3 ops: Set HTTP CORS headers with caching in nginx for services 2024-07-13 20:20:15 +09:00
wiz
5a86c8c83a ops: Set HTTP CORS headers in nginx for services 2024-07-13 19:56:17 +09:00
softsimon
725e9c0d95 restore timeline lowercase time 2024-07-13 19:36:27 +09:00
softsimon
74e59d6ea5 Merge pull request #5333 from mempool/natsoni/timeline-updates
Acceleration timeline refactor
2024-07-13 18:53:39 +09:00
natsoni
9ac45a6cc3 Clear interval on destroy and remove commented code 2024-07-13 18:45:18 +09:00
wiz
94d537daa6 Merge pull request #5330 from mempool/simon/block-preview-miner-tag-design
Block preview new miner tag design
2024-07-13 18:37:45 +09:00
wiz
30bc026c28 Merge pull request #5331 from mempool/mononaut/accelerated-cpfp
show cpfp toggle on pending accelerations
2024-07-13 18:37:23 +09:00
wiz
21942f8ab1 Merge pull request #5334 from mempool/natsoni/click-on-acceleration-graph
Allow to click on bid boost graph to go on block page
2024-07-13 18:36:47 +09:00
wiz
147f55fec3 Merge pull request #5335 from mempool/natsoni/fix-btc-amount-pool
Fix btc amount in mining dashboard
2024-07-13 18:36:04 +09:00
natsoni
e73f2cbdc1 Fix btc amount mining dashboard 2024-07-13 18:22:45 +09:00
softsimon
009e18b622 adjust lightning dashboard to be in line with new default dashboard 2024-07-13 18:18:56 +09:00
natsoni
5f20803e21 Allow to click on bid boost graph to go on block page 2024-07-13 18:16:41 +09:00
softsimon
832d16cf2d Merge pull request #5332 from hans-crypto/html-quickfix
Html quickfix
2024-07-13 17:38:10 +09:00
natsoni
5e8b5e75d8 Fix accelerated fee update logic 2024-07-13 17:31:49 +09:00
Hans ❤️ Crypto
c5ef1011d8 Merge branch 'mempool:master' into html-quickfix 2024-07-13 10:23:28 +02:00
softsimon
6a14043641 Merge pull request #5094 from ordpool-space/hans-crypto-patch-1
Remove reference to bisq in unfurler
2024-07-13 17:09:21 +09:00
Mononaut
0f526f24cb show cpfp toggle on pending accelerations 2024-07-13 07:59:59 +00:00
softsimon
4426bb10a9 Block preview new miner tag design 2024-07-13 16:48:03 +09:00
natsoni
18a7859cca Merge branch 'master' into natsoni/timeline-updates 2024-07-13 16:25:14 +09:00
natsoni
349d491f7d Refactor timeline but keep times 2024-07-13 16:21:56 +09:00
wiz
7556424f0b Merge pull request #5328 from mempool/nymkappa/accel-dashboard-update
[accelerator] also show completed_provisional in accel dashboard
2024-07-13 16:03:16 +09:00
wiz
1d827a9724 Merge pull request #5329 from mempool/nymkappa/external-menu-link
[menu] link can be external
2024-07-13 16:01:10 +09:00
softsimon
f019dd67b3 update i18n from transifex 2024-07-13 14:38:27 +09:00
Jameson Lopp
8b27ac1bbf add contributor agreement 2024-07-12 17:03:27 -04:00
nymkappa
b91774d50c [menu] link can be external 2024-07-13 00:41:45 +09:00
nymkappa
22a5cd2de2 [accelerator] also show completed_provisional in accel dashboard 2024-07-12 23:45:41 +09:00
softsimon
e5489277c6 i18n fixes 2024-07-12 23:20:18 +09:00
softsimon
04b6bee8a1 Merge pull request #5327 from mempool/revert-5321-natsoni/fees-on-acc-timeline
Revert "Show accelerated fee rates on timeline"
2024-07-12 19:10:17 +09:00
softsimon
6cd8cf660b Revert "Show accelerated fee rates on timeline" 2024-07-12 19:10:06 +09:00
wiz
1b6fd29c82 Merge pull request #5325 from mempool/mononaut/subnet-route-restrictions
Restrict accelerator routes to mainnet
2024-07-12 18:59:21 +09:00
softsimon
a31dae67a8 Merge pull request #5326 from mempool/revert-5323-natsoni/timeline-feedback
Revert "Add accelerated word to timeline"
2024-07-12 18:58:20 +09:00
softsimon
76e3053207 Revert "Add accelerated word to timeline" 2024-07-12 18:58:02 +09:00
Mononaut
985b7577e4 Restrict accelerator routes to mainnet 2024-07-12 09:29:21 +00:00
wiz
c748e5cda9 Merge pull request #5323 from mempool/natsoni/timeline-feedback 2024-07-12 18:12:27 +09:00
natsoni
a99f45cd47 Add accelerated word to timeline 2024-07-12 18:00:02 +09:00
softsimon
de1d7839b3 Merge pull request #5321 from mempool/natsoni/fees-on-acc-timeline
Show accelerated fee rates on timeline
2024-07-12 17:24:36 +09:00
natsoni
6aa3e38af2 Fix broken loader in accelerate fee rate line 2024-07-12 17:15:51 +09:00
softsimon
dca7df709b Merge pull request #5305 from mempool/natsoni/avoid-fetching-full-audit
Avoid fetching full audit on tx page
2024-07-12 17:10:36 +09:00
softsimon
e40e9f7d11 Merge pull request #5319 from mempool/orangesurf/accelerator-api
Update Accelerator API documentation
2024-07-12 17:04:28 +09:00
softsimon
285bb357ba Merge pull request #5317 from mempool/mononaut/coming-now
[accelerator] remove "coming soon" button state
2024-07-12 16:34:21 +09:00
natsoni
c3b9828d42 Move block audit cache to apiService 2024-07-12 16:00:51 +09:00
softsimon
871e590305 Merge pull request #5322 from mempool/natsoni/fix-accelerations-graph
Fix accelerations graph view more
2024-07-12 15:08:59 +09:00
softsimon
0e5a1abb2b Merge pull request #5320 from mempool/mononaut/24h-acc-dash
Add 24h and all time views to accelerator dashboard
2024-07-12 15:02:46 +09:00
natsoni
5665c6e6ec Fix accelerations graph view more 2024-07-12 14:20:05 +09:00
natsoni
06b696f0bb Show fees rates on acceleration timeline 2024-07-12 01:58:20 +09:00
Mononaut
75ca963bd5 Add 24h and all time views to accelerator dashboard 2024-07-11 16:48:41 +00:00
orangesurf
3a6647eac0 Update Accelerator APIs 2024-07-11 18:12:23 +02:00
Mononaut
9ad6b925c8 [accelerator] remove "coming soon" button state 2024-07-11 12:27:31 +00:00
natsoni
17720b98c1 Avoid briefly displaying wrong accelerated fee rate on tx load 2024-07-11 20:38:38 +09:00
wiz
5bb3e930cc Merge pull request #5313 from mempool/mononaut/enable-cashapp
[accelerator] enable cashapp
2024-07-11 20:28:43 +09:00
wiz
347bddc974 Merge pull request #5315 from mempool/hunicus/faq-update-accelerate
Update faq
2024-07-11 20:28:16 +09:00
hunicus
4eca8240db Update accelerator faq mention for public availability 2024-07-11 18:18:13 +09:00
Mononaut
1c135b4c67 [accelerator] enable cashapp 2024-07-11 07:56:15 +00:00
softsimon
f24223ca06 Merge pull request #5312 from mempool/mononaut/fix-enterprise-import
Fix broken enterpriseService import
2024-07-11 15:06:28 +09:00
Mononaut
9748aa05cf Fix broken enterpriseService import 2024-07-11 05:59:29 +00:00
wiz
1a5613bf65 Merge pull request #5311 from mempool/natsoni/accel-tx-fee-update
Update tx acceleration state on confirmation
2024-07-11 14:50:06 +09:00
wiz
e55e4e378a Merge pull request #5310 from mempool/mononaut/acc-goal
accelerator goals
2024-07-11 01:21:15 +09:00
Mononaut
927eb98072 accelerator goals 2024-07-10 16:18:13 +00:00
natsoni
99ea1ad0a0 Avoid fetching full audit on tx page 2024-07-11 00:23:46 +09:00
softsimon
fed3012449 prevent goggles from becoming small or move with many filters activated 2024-07-10 23:26:34 +09:00
natsoni
bbff50527b Don't show Accelerated on tx just mined by non-participating pool 2024-07-10 23:23:40 +09:00
natsoni
4470461a98 Add retry logic to acceleration data fetching on tx page 2024-07-10 23:22:57 +09:00
natsoni
645fd98c30 Show actual accelerated fee rate on newly mined tracked tx 2024-07-10 23:21:53 +09:00
softsimon
10de603ee7 use default link color for top up link 2024-07-10 23:16:28 +09:00
softsimon
685c1c9fb2 Merge pull request #5308 from mempool/revert-5306-mononaut/selected-block-pool
Revert "align block arrows & reposition selected block pool tag"
2024-07-10 21:51:17 +09:00
softsimon
d02a67766d Revert "align block arrows & reposition selected block pool tag" 2024-07-10 21:51:04 +09:00
wiz
7721fde7b6 Merge pull request #5306 from mempool/mononaut/selected-block-pool
align block arrows & reposition selected block pool tag
2024-07-10 21:33:02 +09:00
wiz
aa10d1233c Merge pull request #5304 from mempool/natsoni/fix-miner-tag-loading
Possibly fix miner tag loading on tracked transactions
2024-07-10 21:31:56 +09:00
orangesurf
ba79821aac 20240710 Update ToS and PP (#5307) 2024-07-10 21:29:02 +09:00
Mononaut
e054e1d5a3 align block arrows & reposition selected block pool tag 2024-07-10 08:15:29 +00:00
softsimon
565910f9f9 Merge pull request #5303 from mempool/mononaut/oob-8dp
always show out-of-band block fees to 8 decimal places
2024-07-10 13:24:53 +09:00
natsoni
2915be8fd6 Possibly fix miner tag loading on tracked transactions 2024-07-10 12:55:34 +09:00
Mononaut
ff25b8ff1e always show out-of-band block fees to 8 decimal places 2024-07-10 03:51:51 +00:00
softsimon
2d03ab6346 make arrow position more consistent
fixes #5180
2024-07-10 12:49:16 +09:00
wiz
a530b70f9f Merge pull request #5302 from mempool/simon/smaller-block-arrow
Smaller block arrow
2024-07-10 01:46:42 +09:00
softsimon
986d71d47f Smaller block arrow 2024-07-10 01:44:15 +09:00
wiz
79f4720516 Merge pull request #5299 from mempool/mononaut/services-api-config
services api endpoint config
2024-07-09 23:51:53 +09:00
wiz
6135b1db10 Merge pull request #5300 from mempool/simon/pool-search-icons
Icons to pool search
2024-07-09 23:51:05 +09:00
wiz
4269077d4b Merge pull request #5301 from mempool/natsoni/hide-standard-eta-timeline
Remove standard ETA from timeline
2024-07-09 23:50:47 +09:00
natsoni
da0df70ad2 Acc timeline: More similar color logic with RBF 2024-07-09 22:14:40 +09:00
softsimon
6253d3716d Icons to pool search 2024-07-09 21:52:19 +09:00
Mononaut
614432426a call services api directly, make endpoint configurable 2024-07-09 12:23:52 +00:00
softsimon
e51951c3ff Merge pull request #5298 from svrgnty/master
add seconds to address and transaction views
2024-07-09 21:02:34 +09:00
natsoni
b38bf0f7b6 Hide standard ETA data until proper ETA calculation gets implemented 2024-07-09 20:50:47 +09:00
svrgnty
503de93094 add seconds to address and transaction views 2024-07-09 13:10:48 +02:00
natsoni
58f3169712 Faster, synced chevron animation 2024-07-09 18:48:58 +09:00
natsoni
53da6549e2 Remove unused CSS 2024-07-09 18:48:33 +09:00
wiz
65046c4cb8 Change blockstream/electrs to mempool/electrs in README 2024-07-09 18:03:41 +09:00
Mononaut
9416fd25f4 [accelerator] tidy up chevron animation 2024-07-09 08:58:03 +00:00
Mononaut
adde1a86e4 [accelerator] fast track chevrons animation 2024-07-09 08:57:09 +00:00
wiz
8a96669260 Merge pull request #5296 from mempool/mononaut/disable-services-proxy
[ops] disable node services api proxy on production
2024-07-09 15:38:38 +09:00
Mononaut
92434d41a4 [ops] disable services api proxy on production 2024-07-09 06:27:26 +00:00
softsimon
2c81ebb637 Merge pull request #5294 from mempool/mononaut/acc-fee-graph-fixes
[accelerator] improve rendering of acceleration fee rate graph
2024-07-09 01:01:27 +09:00
wiz
7735da96f2 Merge pull request #5293 from mempool/simon/block-mining-pool-logos
Block pool logos [Test]
2024-07-09 00:10:06 +09:00
softsimon
d914df20ba updating miner tag on tx page 2024-07-09 00:01:20 +09:00
softsimon
852e2b2fa0 miner tag as texts instead of badges 2024-07-08 23:44:22 +09:00
Mononaut
9396a4bbae [accelerator] improve rendering of acceleration fee rate graph 2024-07-08 14:36:38 +00:00
wiz
bf95938be8 Merge branch 'master' into simon/block-mining-pool-logos 2024-07-08 23:06:28 +09:00
wiz
8d2e7bef7a Merge pull request #5287 from mempool/natsoni/acc-timeline-polish
Acceleration timeline polishing
2024-07-08 23:06:09 +09:00
wiz
6f31fb2a08 Merge branch 'master' into natsoni/acc-timeline-polish 2024-07-08 22:54:31 +09:00
wiz
34b5678199 Merge pull request #5292 from mempool/mononaut/high-fee-accelerations
[accelerator] hide modal for transactions near the top of the mempool
2024-07-08 22:53:53 +09:00
softsimon
432496d2a0 move logo into the badge 2024-07-08 22:53:03 +09:00
natsoni
23ee613414 Fix missing 'Mined' tag 2024-07-08 22:49:31 +09:00
softsimon
c391a532de fix overflow 2024-07-08 22:29:49 +09:00
natsoni
cd56128bb6 Implement feedbacks on acceleration timeline 2024-07-08 22:17:18 +09:00
wiz
07370a8dc7 Merge branch 'master' into natsoni/acc-timeline-polish 2024-07-08 21:58:34 +09:00
natsoni
bf51e3e1c9 Show unaccelerated ETA in acceleration timeline 2024-07-08 21:53:41 +09:00
softsimon
eec6efcc22 Block pool logos 2024-07-08 21:45:57 +09:00
Hans ❤️ Crypto
64dd55b44b Update block.component.html 2024-07-08 12:40:43 +02:00
Hans ❤️ Crypto
e2d2a8da26 Update block-transactions.component.html 2024-07-08 12:39:49 +02:00
Mononaut
487d82eccf [accelerator] hide modal for transactions near the top of the mempool 2024-07-08 09:45:49 +00:00
softsimon
c43b567847 extracting i18n 2024-07-08 18:00:55 +09:00
wiz
5d9c846a8f Merge pull request #5290 from mempool/mononaut/server-side-bids 2024-07-08 17:50:46 +09:00
wiz
cc30536857 Merge pull request #5291 from mempool/simon/acc-error-message-positioning 2024-07-08 17:49:00 +09:00
wiz
8625419417 Merge pull request #5288 from mempool/natsoni/fix-statistics-replication 2024-07-08 17:47:17 +09:00
natsoni
a9341821c5 Remove 211.fra from trusted servers list 2024-07-08 17:36:25 +09:00
softsimon
d074ff1d4c Fix accelerator error message positioning 2024-07-08 16:38:38 +09:00
Mononaut
9837a69a1a [accelerator] move bid option calculation to server side 2024-07-08 05:18:42 +00:00
softsimon
32eaf29aaa fix i18n error rendering 2024-07-08 12:48:29 +09:00
softsimon
5316d1705a pulling new i18n 2024-07-08 11:42:57 +09:00
softsimon
2fb735c430 adding missing i18n 2024-07-07 23:01:55 +09:00
natsoni
5001d553f3 Quick fix on statistics replication: filter out temporal outsiders 2024-07-07 22:35:34 +09:00
softsimon
663a09ea97 i18n extraction 2024-07-07 18:59:22 +09:00
softsimon
8afdd9a482 Merge pull request #5285 from mempool/mononaut/acc-timeout
[accelerator] error message after timeout
2024-07-07 18:58:28 +09:00
softsimon
fc12733132 Merge pull request #5286 from mempool/mononaut/accelerator-unavailable
[accelerator] handle temporarily unavailable state
2024-07-06 20:00:06 +09:00
Mononaut
0c200e090d [accelerator] handle temporarily unavailable state 2024-07-06 10:48:37 +00:00
Mononaut
f4a9aeacc7 [accelerator] error message after timeout 2024-07-06 08:32:33 +00:00
softsimon
1a2487b740 Merge pull request #5284 from mempool/mononaut/zero-seconds
handle zero relative time seconds
2024-07-06 14:54:35 +09:00
Mononaut
3425bdd100 handle zero relative time seconds 2024-07-06 05:48:43 +00:00
softsimon
be72a26760 Merge pull request #5283 from mempool/natsoni/fix-liquid-blocks-page
Fix Liquid blocks page
2024-07-06 00:10:01 +09:00
softsimon
77cd07cc93 Merge pull request #5282 from mempool/mononaut/acc-error-msgs
[accelerator] proper error handling
2024-07-06 00:03:54 +09:00
Mononaut
0e122c15e2 [accelerator] handle estimate api fail 2024-07-05 13:53:03 +00:00
natsoni
2ec0e6634b Fix Liquid blocks page 2024-07-05 22:51:39 +09:00
natsoni
a0992f6091 More accel timeline polish 2024-07-05 22:42:53 +09:00
Mononaut
b8820684c3 [accelerator] proper error handling 2024-07-05 10:42:46 +00:00
natsoni
7c08a104ce remove rtl for now 2024-07-05 16:48:50 +09:00
natsoni
fb8bd4b194 Add i18n to acceleration timeline 2024-07-05 16:35:00 +09:00
softsimon
20d948c280 updating i18n 2024-07-05 16:32:26 +09:00
natsoni
1710ae0503 Improve step colors in timeline 2024-07-05 16:30:12 +09:00
softsimon
8735b62510 fix hide acceleration button overflow
fixes #5276
2024-07-05 16:22:28 +09:00
softsimon
4cd70941f7 Merge pull request #5277 from mempool/natsoni/acceleration-timeline
Acceleration timeline concept
2024-07-05 15:49:09 +09:00
softsimon
2773c21343 rename RBF history to timeline 2024-07-05 15:47:06 +09:00
wiz
54763fe5d6 Merge pull request #5281 from mempool/mononaut/enable-auto-pools
Rename AUTOMATIC_POOLS_UPDATE, set in prod
2024-07-05 15:43:00 +09:00
Mononaut
99b8a3cb3e Rename AUTOMATIC_POOLS_UPDATE, set in prod 2024-07-05 05:58:14 +00:00
wiz
d15e2ada42 ops: Set HTTP expires header for warm cache mining APIs 2024-07-05 14:44:56 +09:00
softsimon
70548ed532 Merge pull request #5280 from mempool/natsoni/add-first-seen
Add first seen data to confirmed transactions
2024-07-05 14:23:47 +09:00
natsoni
c85e7b08c3 Add first seen data to confirmed transactions 2024-07-05 11:46:30 +09:00
natsoni
bdd51a0f4b Merge branch 'master' into natsoni/acceleration-timeline 2024-07-04 20:55:45 +09:00
wiz
769bb6f1be Merge pull request #5279 from mempool/natsoni/error-message-context
Add more context to error messages
2024-07-04 20:13:37 +09:00
wiz
2fc89f6a35 Merge pull request #5278 from mempool/natsoni/fix-keynav-events
Fix issue on key navigation logic
2024-07-04 20:13:07 +09:00
wiz
f8447c10d5 Merge pull request #5275 from mempool/simon/remove-layer-two
Remove Layer 2 divider
2024-07-04 20:12:36 +09:00
natsoni
3d4316cd44 Add more context to error messages 2024-07-04 19:38:27 +09:00
natsoni
6ed6f2e2cf Fix key navigation logic in blocks-list and recent-pegs-list 2024-07-04 19:03:17 +09:00
softsimon
2b96c99fb3 Merge pull request #5274 from mempool/mononaut/stale-cpfp
Fix stale cpfp bug
2024-07-04 18:45:53 +09:00
natsoni
6b481d5a07 Add acceleration timeline 2024-07-04 18:22:39 +09:00
softsimon
cc77476756 Merge pull request #5272 from mempool/dependabot/npm_and_yarn/backend/ws-8.18.0
Bump ws from 8.17.1 to 8.18.0 in /backend
2024-07-04 18:01:17 +09:00
softsimon
736833b4f6 Remove Layer 2 divider 2024-07-04 17:58:40 +09:00
Mononaut
c37858fa54 Fix stale cpfp bug 2024-07-04 08:43:05 +00:00
wiz
fb44c1d8a8 Merge pull request #5273 from mempool/simon/hide-accelerator-graphs-non-mainnet
Hide accelerator charts on non-mainnet
2024-07-04 17:40:43 +09:00
natsoni
815dcbd4ce Retrieve acceleration request time and first seen time 2024-07-04 16:54:03 +09:00
softsimon
3c6e18f198 Hide accelerator charts on non-mainnet
fixes #5265
2024-07-04 11:43:31 +09:00
dependabot[bot]
fa84283a01 Bump ws from 8.17.1 to 8.18.0 in /backend
Bumps [ws](https://github.com/websockets/ws) from 8.17.1 to 8.18.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.17.1...8.18.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-04 02:13:31 +00:00
softsimon
46c4d57367 updating german i18n 2024-07-04 00:49:22 +09:00
softsimon
7dfb3c452f add some margin left to mining pie chart 2024-07-03 22:42:45 +09:00
softsimon
9be56badee updating japanese 2024-07-03 22:37:42 +09:00
softsimon
bbdc9e4aa4 fix accelerator logo positioning 2024-07-03 22:02:27 +09:00
softsimon
a3e58d632e add svg titles 2024-07-03 22:00:54 +09:00
wiz
df7e647523 Merge pull request #5271 from mempool/hunicus/add-logo-tm
Update trademark images
2024-07-03 21:47:41 +09:00
hunicus
c9edfa1826 Add logos to general info text section 2024-07-03 21:42:09 +09:00
hunicus
9ebb98b1b9 Revert horizontal logo change 2024-07-03 21:37:04 +09:00
softsimon
cc5ccd01e2 turn mempool accelerator logo into a link 2024-07-03 21:30:59 +09:00
softsimon
c34be2a334 fix malplaced details button 2024-07-03 21:18:33 +09:00
softsimon
1270a2d67a Pull from transifex 2024-07-03 21:17:02 +09:00
hunicus
5a4b79b83e Add accelerator & goggle logos to trademark-policy
Also update horizontal mempool.space logo to correct
font weight.
2024-07-03 21:07:02 +09:00
softsimon
2fc0079530 Accelerator mobile size 2024-07-03 20:50:02 +09:00
wiz
680d8504b6 Merge pull request #5268 from mempool/mononaut/paid-processing
accelerator success screen
2024-07-03 19:47:15 +09:00
wiz
89db3dc70e Merge pull request #5269 from mempool/simon/mempool-goggles-logo
Update mempool goggles logo
2024-07-03 19:45:53 +09:00
wiz
9d6816132b Merge pull request #5270 from mempool/simon/accelerate-logo-wip
Accelerate logo
2024-07-03 19:45:25 +09:00
softsimon
f496fc9653 Accelerate logo 2024-07-03 19:30:32 +09:00
softsimon
9318aa9a6a Update mempool goggles logo 2024-07-03 19:17:18 +09:00
Mononaut
db3db49fbc [accelerator] success confirmation screen 2024-07-03 18:16:50 +09:00
Mononaut
75ad6a2335 [accelerator] remove green success banner 2024-07-03 18:15:57 +09:00
softsimon
ec209bb618 new eta i18n key 2024-07-03 17:32:22 +09:00
softsimon
2b21ddd0b2 Merge pull request #5263 from mempool/simon/new-eta-label
New ETA label
2024-07-03 17:31:35 +09:00
softsimon
d0358f1551 Merge pull request #5262 from mempool/simon/accelerator-default-hide
Only default show accelerator on mempool space
2024-07-03 17:31:26 +09:00
softsimon
6ce1970ef4 Merge pull request #5260 from mempool/simon/tx-page-ui-jump
Fix accelerator ui jumps
2024-07-03 17:31:07 +09:00
softsimon
6597854b14 Fix accelerator ui jumps 2024-07-03 17:30:31 +09:00
softsimon
69cd054a97 Merge pull request #5267 from mempool/mononaut/fix-ln-invoice-flicker
fix ln invoice flicker
2024-07-03 17:29:43 +09:00
softsimon
0ea22961e8 fix i18n duplicate 2024-07-03 17:28:00 +09:00
Mononaut
1ce72e23a3 [accelerator] fix ln invoice flicker 2024-07-03 17:19:22 +09:00
softsimon
140c371c51 fix duplicate i18n 2024-07-03 16:24:45 +09:00
softsimon
39e55bb3f8 New ETA label 2024-07-03 15:59:54 +09:00
softsimon
5a9dde0807 Only default show accelerator on mempool space 2024-07-03 12:27:29 +09:00
softsimon
ae8b6043b2 Merge pull request #5261 from mempool/dependabot/npm_and_yarn/frontend/esbuild-0.23.0
Bump esbuild from 0.22.0 to 0.23.0 in /frontend
2024-07-03 11:54:56 +09:00
dependabot[bot]
b49618fbed Bump esbuild from 0.22.0 to 0.23.0 in /frontend
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-03 02:29:29 +00:00
softsimon
46e8f6137c Merge pull request #5259 from mempool/mononaut/fix-auth-refresh
fix auth refresh race condition
2024-07-02 22:11:34 +09:00
Mononaut
ec2ab174de fix auth refresh race condition 2024-07-02 13:08:20 +00:00
wiz
4e18ff3329 Merge pull request #5258 from mempool/nymkappa/btcpayid
[btcpay] temp fix qr code accel
2024-07-02 22:04:24 +09:00
nymkappa
90a8ff47b7 [btcpay] temp fix qr code accel 2024-07-02 22:03:44 +09:00
wiz
c4f5aa1874 Merge pull request #5253 from mempool/natsoni/fix-accelerator-dashboard
Fix key navigation bug in accelerator dashboard
2024-07-02 21:43:36 +09:00
wiz
3c106a3c8f Merge pull request #5254 from mempool/nymkappa/hide-accel-menu
[services] hide accelerator from user menu if not whitelisted
2024-07-02 21:43:26 +09:00
wiz
ec033a9eaf Merge pull request #5257 from mempool/mononaut/more-accelerator-polish
more accelerator polish
2024-07-02 21:43:02 +09:00
softsimon
6b0496029c Filter arrow key strokes 2024-07-02 21:42:11 +09:00
Mononaut
642bf86423 [accelerator] streamline payment method logic 2024-07-02 12:32:09 +00:00
Mononaut
3e07d6b684 [accelerator] disable for txs not in mempool 2024-07-02 12:32:08 +00:00
softsimon
3a94687548 Merge pull request #5248 from mempool/nymkappa/fix-btcpay-invoice-amount
[btcpay] cleanup invoice api
2024-07-02 21:32:01 +09:00
softsimon
8028d80ab8 Merge pull request #5256 from mempool/nymkappa/more-auth-fix
[auth] more auth fixes
2024-07-02 21:31:36 +09:00
softsimon
453bcffbbc mandarin corrections 2024-07-02 21:23:43 +09:00
nymkappa
c6b2db9282 [auth] more auth fixes 2024-07-02 21:20:18 +09:00
nymkappa
00fb261124 [services] hide accelerator from user menu if not whitelisted 2024-07-02 20:43:37 +09:00
nymkappa
11113041bb Merge branch 'master' into nymkappa/fix-btcpay-invoice-amount 2024-07-02 20:34:42 +09:00
softsimon
63cb6c3804 Merge pull request #5250 from mempool/nymkappa/accel-checkout-clear-error
[accelerator] clear error state when auth state changes
2024-07-02 18:26:05 +09:00
softsimon
2ff3d00bd7 Merge pull request #5249 from mempool/nymkappa/fix-auth-issue
[auth] catch auth error and return null
2024-07-02 18:23:12 +09:00
softsimon
f0a63aaba3 Merge pull request #5251 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.13.0
Bump cypress from 13.12.0 to 13.13.0 in /frontend
2024-07-02 18:22:05 +09:00
softsimon
44ef81fde0 Swedish, Chinese etc 2024-07-02 17:29:08 +09:00
softsimon
16caae8123 i18n fix 2024-07-02 17:11:50 +09:00
softsimon
5a897e56ab i18n fix 2024-07-02 17:05:56 +09:00
softsimon
5715915850 add missing i18n 2024-07-02 16:46:49 +09:00
softsimon
53109aa50a fixing i18n 2024-07-02 15:54:49 +09:00
softsimon
d52ca35cc0 i18n fixes 2024-07-02 15:32:03 +09:00
softsimon
d00f4245f8 i18n fixes 2024-07-02 15:15:59 +09:00
natsoni
4723ca88ec Fix key navigation bug in accelerator dashboard 2024-07-02 15:04:54 +09:00
softsimon
e5d23e8076 Merge pull request #5252 from mempool/natsoni/fix-bech32
Fix bech32 regex and adapt tests
2024-07-02 14:33:53 +09:00
softsimon
2827dcd0ba i18n fixes 2024-07-02 14:02:16 +09:00
natsoni
7d7f9b1665 Fix bech32 regex and adapt tests 2024-07-02 13:09:05 +09:00
dependabot[bot]
2eb9108046 Bump cypress from 13.12.0 to 13.13.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.12.0 to 13.13.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.12.0...v13.13.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-02 02:53:56 +00:00
nymkappa
b198528592 [accelerator] clear error state when auth state changes 2024-07-02 11:21:30 +09:00
nymkappa
7dcd952a40 [btcpay] cleanup invoice api 2024-07-02 11:12:20 +09:00
nymkappa
89a3b1c577 [auth] catch auth error and return null 2024-07-02 10:37:40 +09:00
softsimon
3dbbc83077 Updating i18n strings 2024-07-01 22:54:24 +09:00
softsimon
011a854a84 Merge pull request #5244 from mempool/nymkappa/refresh-checkout-state-logout
[accelerator] refresh checkout state logout
2024-07-01 19:08:07 +09:00
softsimon
6261f83e5e Merge pull request #5246 from mempool/nymkappa/update-payment-method-handling
[accelerator] update payment method handling
2024-07-01 19:06:26 +09:00
softsimon
c4b45180dd Merge pull request #5241 from vostrnad/baremultisig-labels
Fix missing bare multisig labels
2024-07-01 19:06:07 +09:00
nymkappa
69b40cf073 [accelerator] add new error message payment_method_not_allowed_out_of_bound 2024-07-01 18:30:40 +09:00
nymkappa
9ef79a268d [accelerator] update payment method handling 2024-07-01 18:18:13 +09:00
softsimon
75c9e15e16 Merge branch 'master' into nymkappa/refresh-checkout-state-logout 2024-07-01 18:00:30 +09:00
softsimon
dfede7fe25 Merge pull request #5243 from mempool/mononaut/hybrid-accelerator-polish
Accelerator polish
2024-07-01 18:00:08 +09:00
softsimon
a86709d7b0 Merge pull request #5245 from mempool/mononaut/no-replaceable-inputs
don't accelerate txs with replaceable inputs
2024-07-01 17:07:42 +09:00
Mononaut
396eee3555 [accelerator] hide accelerate button for ineligible txs 2024-07-01 07:42:57 +00:00
Mononaut
5067c88642 [accelerator] check for high sigops 2024-07-01 07:39:28 +00:00
Mononaut
e35ac6e1a2 [accelerator] check for input replaceability 2024-07-01 07:28:25 +00:00
nymkappa
5b93c8e875 [accelerator] refresh auth state when logging out 2024-07-01 16:21:47 +09:00
Mononaut
c71a0afe1f [accelerator] remember hide accelerator preference 2024-07-01 06:44:03 +00:00
nymkappa
2d12d2e5ef [logout] fix redirection 2024-07-01 15:30:39 +09:00
Mononaut
23fa28567d [accelerator] toggle button alignment 2024-07-01 06:19:29 +00:00
Mononaut
a624e82630 [accelerator] restore "wait" radio on pizza tracker 2024-07-01 06:19:11 +00:00
Mononaut
da4c2f5307 [accelerator] remove safety catch, always show checkout 2024-07-01 05:45:32 +00:00
nymkappa
b91f195955 [footer] refresh auth state in real time 2024-07-01 14:33:19 +09:00
Mononaut
69b346ab00 move CPFP panel above accelerator 2024-07-01 05:24:21 +00:00
Vojtěch Strnad
1c89a1a44e Fix missing bare multisig labels 2024-07-01 07:21:37 +02:00
Mononaut
3088befbf5 remove btcpay.svg 2024-07-01 05:18:19 +00:00
softsimon
7ed35b955d Merge pull request #5239 from mempool/dependabot/docker/docker/frontend/node-20.15.0-buster-slim
Bump node from 20.14.0-buster-slim to 20.15.0-buster-slim in /docker/frontend
2024-07-01 12:59:19 +09:00
softsimon
e7e4b63fbc Merge pull request #5238 from mempool/dependabot/docker/docker/backend/node-20.15.0-buster-slim
Bump node from 20.14.0-buster-slim to 20.15.0-buster-slim in /docker/backend
2024-07-01 12:59:08 +09:00
softsimon
723ac4cece Merge pull request #5240 from mempool/dependabot/npm_and_yarn/frontend/esbuild-0.22.0
Bump esbuild from 0.21.1 to 0.22.0 in /frontend
2024-07-01 12:58:49 +09:00
dependabot[bot]
1a91f2b0a3 Bump esbuild from 0.21.1 to 0.22.0 in /frontend
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.21.1 to 0.22.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.1...v0.22.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:49:42 +00:00
softsimon
300bfd225b Merge pull request #5207 from mempool/mononaut/pool-reindexing
Pool reindexing
2024-07-01 11:32:03 +09:00
mononaut
cf09669902 Merge branch 'master' into mononaut/pool-reindexing 2024-07-01 11:25:02 +09:00
dependabot[bot]
d5525ae324 Bump node in /docker/frontend
Bumps node from 20.14.0-buster-slim to 20.15.0-buster-slim.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:16:38 +00:00
dependabot[bot]
ff8b0a8d80 Bump node in /docker/backend
Bumps node from 20.14.0-buster-slim to 20.15.0-buster-slim.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:11:21 +00:00
softsimon
8fc5fdbde6 Merge pull request #5237 from vostrnad/p2tr-without-witness
Fix errors caused by P2TR inputs without witness data
2024-07-01 10:48:16 +09:00
Mononaut
27c70bd919 Also fix backend errors caused by P2TR inputs without witness data 2024-07-01 01:22:10 +00:00
Vojtěch Strnad
7432e6e29b Fix errors caused by P2TR inputs without witness data 2024-07-01 02:02:53 +02:00
wiz
a9c3637c7f Merge pull request #5233 from mempool/mononaut/hybrid-acceleration-checkout
hybrid acceleration checkout
2024-07-01 00:27:53 +09:00
softsimon
de95dd9c77 removing margin causing table jump 2024-06-30 22:20:44 +09:00
Mononaut
da1e5c515e [accelerator] use invoice amount 2024-06-30 12:44:32 +00:00
Mononaut
ce879152fd [accelerator] don't scroll to btcpay invoice 2024-06-30 12:43:38 +00:00
Mononaut
d76490df0c [accelerator] fresh invoice after changing bid 2024-06-30 12:43:31 +00:00
Mononaut
a80372f335 [accelerator] play sound on invoice paid 2024-06-30 12:43:26 +00:00
softsimon
102625b3ea Merge pull request #5236 from mempool/mononaut/fix-weird-dust
Fix dust limit for undefined witness program outputs
2024-06-30 18:34:42 +09:00
softsimon
eb3c248acd Add test transaction 2024-06-30 18:34:22 +09:00
Mononaut
9140bcb408 [accelerator] fix liquid 2024-06-30 08:58:39 +00:00
Mononaut
35d0e7fae7 [accelerator] rerefactor bitcoin-payment component 2024-06-30 08:39:32 +00:00
Mononaut
f114a8ca75 [accelerator] refactor bitcoin-payment component 2024-06-30 08:15:20 +00:00
Mononaut
0b663c1a77 [accelerator] fix loading spinner alignment 2024-06-30 07:41:25 +00:00
Mononaut
c494207469 [accelerator] match loading height to actual QR 2024-06-30 07:38:25 +00:00
Mononaut
ce31d0512c [accelerator] improve btcpay QR codes 2024-06-30 07:32:04 +00:00
Mononaut
3ecc8ae8cf [accelerator] ln qr 2024-06-30 07:17:15 +00:00
Mononaut
1e820a0fc8 [accelerator] soft enforce referrer 2024-06-30 06:56:55 +00:00
Mononaut
e3abdf4b4f [accelerator] revert titles 2024-06-30 06:56:55 +00:00
Mononaut
caf7011df5 [accelerator] checkbox error hint 2024-06-30 06:56:54 +00:00
softsimon
7caad9fca9 Merge pull request #5234 from mempool/nymkappa/fix-bitcoin-rounding
[btcpay] fix displayed amount
2024-06-30 14:54:26 +09:00
Mononaut
84e1ac31c2 [accelerator] fix stray margin 2024-06-30 05:51:13 +00:00
Mononaut
3b4ac3b6b7 [accelerator] less muted text 2024-06-30 05:42:32 +00:00
Mononaut
cfe5da2276 [accelerator] streamline flow 2024-06-30 05:37:51 +00:00
Mononaut
110b7a934c [accelerator] buttons 2024-06-30 04:57:00 +00:00
Mononaut
d059c5ca27 [accelerator] slim summary screen 2024-06-30 03:43:28 +00:00
Mononaut
bf37affe47 [accelerator] fiat limits 2024-06-30 03:23:09 +00:00
Mononaut
2798b43913 [accelerator] adjust h1 labels 2024-06-30 02:40:58 +00:00
Mononaut
425edb9b9f Fix dust limit for undefined witness program outputs 2024-06-30 02:06:50 +00:00
Mononaut
f68c8cc621 [accelerator] restore scroll events, remove eta button 2024-06-30 01:46:11 +00:00
Mononaut
c5fc476834 [accelerator] no autoscroll to checkout 2024-06-29 09:21:39 +00:00
Mononaut
776404dbde [accelerator] Pro for everyone 2024-06-29 09:17:08 +00:00
nymkappa
1067131120 [btcpay] fix displayed amount 2024-06-29 16:47:38 +09:00
Mononaut
6cf753ddaf [accelerator] fix other missing button 2024-06-29 07:46:24 +00:00
Mononaut
277f8f7bfd [accelerator] restore missing sparkles button 2024-06-29 07:45:10 +00:00
Mononaut
c249da7901 [accelerator] pizza tracker waitlisted & preview-only screens 2024-06-29 07:13:43 +00:00
Mononaut
3720d67666 [accelerator] waitlisted & preview-only screens 2024-06-29 07:04:08 +00:00
Mononaut
84d4eaa932 remove stray console.log 2024-06-29 06:08:58 +00:00
Mononaut
d62300ccff [accelerator] add acceleration paid screen, fix end state 2024-06-29 06:06:11 +00:00
Mononaut
48bdae4e78 [accelerator] hide pizza tracker CTA when irrelevant 2024-06-29 04:11:02 +00:00
Mononaut
193c41cb81 Fix pizza tracker loading state 2024-06-29 04:10:47 +00:00
Mononaut
5872b2c46b [accelerator] fix success/failure messages 2024-06-28 13:46:02 +00:00
Mononaut
e158c10688 [accelerator] fix duplicate invoice request 2024-06-28 13:46:02 +00:00
wiz
b4e46c3ff8 Merge branch 'master' into mononaut/hybrid-acceleration-checkout 2024-06-28 21:26:23 +09:00
nymkappa
254d962558 [accelerator] add new error message 2024-06-28 07:06:02 +00:00
Mononaut
c75afe20cd More acceleration checkout refactoring 2024-06-28 07:05:57 +00:00
wiz
98e9d1a6c3 Merge pull request #5227 from mempool/hunicus/about-juggling
Juggle community integration listings
2024-06-28 15:45:39 +09:00
hunicus
c4577b8c09 Merge branch 'master' into hunicus/about-juggling 2024-06-28 15:36:00 +09:00
hunicus
95c4da51ed Juggle community integration listings
Also add back bitcoin-s and remove mercury.

Signed-off-by: hunicus <93150691+hunicus@users.noreply.github.com>
2024-06-28 15:30:43 +09:00
softsimon
2e336d7ad1 Merge pull request #5231 from mempool/hunicus/about-foundry-logo
Update foundry logo on about page
2024-06-28 14:07:48 +09:00
hunicus
903ff1ea66 Update foundry logo on about page 2024-06-28 13:49:13 +09:00
softsimon
f02d8e0626 Merge pull request #5230 from mempool/simon/docs-root-network-support
Docs root network support
2024-06-28 11:43:05 +09:00
softsimon
ea04ea0048 Docs root network support 2024-06-28 11:28:41 +09:00
Mononaut
473da82caa acceleration estimate payment methods field 2024-06-27 13:09:43 +00:00
Mononaut
415ad3de70 Merge simple & advanced acceleration checkout components 2024-06-27 13:09:39 +00:00
softsimon
d91c6bceed Merge pull request #5226 from mempool/natsoni/fix-accel-pie-chart
Fix logic for pool pie chart position
2024-06-27 21:48:04 +09:00
softsimon
aa5355e93d Merge pull request #5229 from rishkwal/rishkwal/fix-tx-base-route
Redirect user to `/` when user goes to `/tx` without any transaction `id`
2024-06-27 21:44:27 +09:00
softsimon
9672928da9 Adding missing TESTNET4_ENABLED to docker build 2024-06-27 19:19:28 +09:00
Rishabh
d189e70817 fix: redirect /tx/ routes to / 2024-06-27 15:11:00 +05:30
Mononaut
d7acd389bf fix scrolljacking by #accelerate fragment 2024-06-27 09:07:24 +00:00
Mononaut
9fe44bd6ba more simple acceleration UI adjustments 2024-06-27 09:07:24 +00:00
Mononaut
4445fe408b Add simple mode checkout to main transaction page 2024-06-27 09:07:22 +00:00
nymkappa
790e76ab26 [accelerator] add payment methods assets 2024-06-27 09:05:50 +00:00
nymkappa
66a88b8422 [accelerator] accelerate with lightning 2024-06-27 09:05:49 +00:00
natsoni
bb91f9142e Fix accel pool pie chart placement 2024-06-27 17:50:45 +09:00
softsimon
66f90cb0bd Merge pull request #5225 from mempool/natsoni/fix-accel-preview-displaying
Fix acceleration preview showing with fragment on accel txs
2024-06-27 16:50:31 +09:00
softsimon
f1572f0038 Merge pull request #5222 from mempool/mononaut/partition-pool-pie
Show more detail in acceleration pools pie chart
2024-06-27 16:37:28 +09:00
natsoni
c3963d6a0d Fix acceleration preview showing with fragment on accel txs 2024-06-27 16:32:20 +09:00
softsimon
1dd86df3e0 Merge pull request #5224 from mempool/hunicus/about-coldcard
Replace bitcoin-s with coldcard on about page
2024-06-27 16:02:47 +09:00
softsimon
c8d443bea7 Merge pull request #5216 from mempool/natsoni/align-acceleration-pie-chart
Align "Accelerated to / by" fields on mobile
2024-06-27 15:44:03 +09:00
hunicus
575fc737ca Replace bitcoin-s with coldcard on about page 2024-06-27 15:34:53 +09:00
Mononaut
ebd4408b8d Adjust acceleration pool pie labels 2024-06-27 06:19:43 +00:00
Mononaut
d6d8c85419 Show more detail in acceleration pools pie chart 2024-06-27 03:40:03 +00:00
softsimon
fbb409e17b Merge pull request #5219 from mempool/simon/local-accelerator-estimates
Show accelerator estimates on local instances
2024-06-27 11:19:07 +09:00
softsimon
b6d03953b9 Show accelerator estimates on local instances 2024-06-26 21:42:30 +09:00
natsoni
d45104f7c9 Align acceleration pie chart 2024-06-26 18:03:14 +09:00
softsimon
d175c34e5b Merge pull request #5211 from mempool/simon/simpler-advanced-acceleration
Simplify advanced acceleration
2024-06-26 17:42:11 +09:00
softsimon
2bf2440e3a Merge pull request #5215 from mempool/mononaut/acceleration-preview-layout-tweaks
Minor style & layout tweaks for the acceleration preview
2024-06-26 17:41:33 +09:00
Mononaut
124c0acbe1 Minor layout tweaks for the acceleration preview 2024-06-26 08:18:39 +00:00
softsimon
69c5a2fb5a Merge pull request #5214 from mempool/natsoni/rbf-list-loading
Show loading indicator on toggle in RBF list
2024-06-26 17:10:14 +09:00
softsimon
c4f08e0d41 Merge pull request #5213 from mempool/natsoni/accelerations-table-fixes
Add page in URL to accelerations table
2024-06-26 17:08:49 +09:00
natsoni
87ee14f189 Show loading indicator on toggle in RBF list 2024-06-26 16:12:21 +09:00
natsoni
122b4b05c4 Add pagination in URL to accelerations table 2024-06-26 15:37:39 +09:00
natsoni
09f7dddf14 Use url parameter instead of query parameter 2024-06-26 15:25:03 +09:00
softsimon
f7ad45939c Hide surcharge row if zero 2024-06-26 15:08:00 +09:00
natsoni
7b6246a035 Fix loading state in blocks table issue 2024-06-26 14:20:49 +09:00
softsimon
a8d2138404 Simplify advanced acceleration 2024-06-26 12:30:34 +09:00
softsimon
0b608c96dd Merge pull request #5212 from mempool/simon/fix-eta-loading-error
Fix ETA loading error
2024-06-26 12:29:22 +09:00
softsimon
a0402b92f9 Fix ETA loading error 2024-06-26 12:12:49 +09:00
softsimon
14e05b43c7 Merge pull request #5210 from mempool/natsoni/btc-unit-on-pool-page
Force amounts to BTC unit in pool page
2024-06-26 11:46:32 +09:00
natsoni
fc8f8abc7e Add SEO title to Accelerations page 2024-06-26 11:42:22 +09:00
natsoni
a1f1b09c55 Fix loading indicator when changing page 2024-06-26 11:13:32 +09:00
natsoni
ad2d7af084 Force amounts to BTC unit in pool page 2024-06-26 10:48:17 +09:00
softsimon
9d3044efae Merge pull request #5199 from mempool/mononaut/tracker-acceleration-eta
Add projected acceleration ETA to tracker page
2024-06-25 17:15:41 +09:00
wiz
bac21afa54 Merge pull request #5203 from mempool/simon/address-page-romanz-support
Romanz support for address page
2024-06-25 16:54:37 +09:00
softsimon
f98bb675e7 Merge pull request #5209 from mempool/mononaut/fix-type-error
Fix coinbase address type error
2024-06-25 11:25:29 +09:00
Mononaut
3e057f2db1 Fix coinbase address type error 2024-06-25 02:20:44 +00:00
softsimon
4fbdf92f0c Merge pull request #5206 from mempool/simon/address-prefix-fixes
Fix address prefix for non esplora backend
2024-06-25 10:21:50 +09:00
softsimon
fd60940a08 Merge pull request #5167 from mempool/dependabot/npm_and_yarn/unfurler/braces-3.0.3
Bump braces from 3.0.2 to 3.0.3 in /unfurler
2024-06-24 21:21:34 +09:00
Mononaut
c54bc5a4bb Clear redis block cache on pool update 2024-06-24 12:07:52 +00:00
Mononaut
04559e7b98 Update README.md with new mining pool update behavior 2024-06-24 12:07:51 +00:00
Mononaut
255919f03f Update pool instead of deleting blocks 2024-06-24 12:07:51 +00:00
softsimon
b92b5cdd87 Merge pull request #5205 from mempool/mononaut/index-cb-addresses
Add coinbase_addresses to extended blocks & table
2024-06-24 20:59:38 +09:00
Mononaut
03036bf59d coinbase_addresses fixes 2024-06-24 11:51:12 +00:00
softsimon
563def45d8 Fix address prefix for non esplora backend 2024-06-24 18:27:30 +09:00
wiz
e4c9b67239 Merge pull request #5204 from mempool/simon/fix-tx-position-crash
Fix tx position frontend error
2024-06-24 17:26:34 +09:00
softsimon
38bf056b6d Merge pull request #5187 from mempool/simon/websocket-reconnect-root-instance
Prevent websocket reconnect on custom root instances
2024-06-24 17:15:35 +09:00
softsimon
91ddf7ea98 Fix tx position crash 2024-06-24 17:10:33 +09:00
softsimon
a9a1ff68ab Romanz support for address page 2024-06-24 16:25:29 +09:00
softsimon
dfc61f3991 Merge pull request #5202 from mempool/natsoni/round-24h-hashrate
Round 24h pools hashrate
2024-06-24 16:06:10 +09:00
Mononaut
f9d03b1bb4 Add coinbase_addresses to extended blocks & table 2024-06-24 06:15:01 +00:00
softsimon
868dac91c7 Merge pull request #5197 from mempool/simon/sha256-secure-context-workaround
Sha256 P2PK secure context workaround
2024-06-24 13:22:55 +09:00
natsoni
3c689e34b8 Round 24h pools hashrate 2024-06-24 13:22:47 +09:00
softsimon
835f16aab6 Merge pull request #5198 from mempool/natsoni/fix-confirmed-after
Fix "Confirmed after" transaction field
2024-06-24 11:53:50 +09:00
softsimon
2c2a6ee872 Merge pull request #5200 from mempool/mononaut/no-trailing-spaces
no trailing spaces
2024-06-24 11:19:40 +09:00
Mononaut
7d0720d55f no trailing spaces 2024-06-24 02:16:59 +00:00
natsoni
c4dec53387 Fix confirmed after 55 years 2024-06-24 11:06:33 +09:00
Mononaut
517e82ec8b Add projected acceleration ETA to tracker page 2024-06-24 02:06:22 +00:00
softsimon
0c72e1b6ed Merge pull request #5195 from mempool/natsoni/hide-usd-on-non-mainnet
Address balance graph: hide usd on non-mainnet networks
2024-06-24 10:34:30 +09:00
softsimon
5d1877a275 Sha256 P2PK secure context workaround 2024-06-24 09:31:02 +09:00
natsoni
8a43ed1a61 Address balance graph: hide usd on non-mainnet networks 2024-06-23 22:35:00 +09:00
softsimon
61c9debcca Merge pull request #5007 from mempool/nymkappa/prepaid-update-price
[accelerator] change default bid prepaid
2024-06-23 18:51:28 +09:00
wiz
172fb0bf41 Merge pull request #5178 from mempool/mononaut/fix-reorg-health-check
Recover from esplora failover after a reorg to lower height
2024-06-23 18:35:19 +09:00
wiz
eedfbacf01 Merge pull request #5147 from mempool/mononaut/accelerate-preview-hashrate-pie
Acceleration preview hashrate pie chart
2024-06-23 18:34:44 +09:00
Mononaut
396b7eb3d3 Add expected hashrate pie chart & eta to acceleration preview 2024-06-23 09:32:37 +00:00
Mononaut
05724b9d58 Integrate multi-pool ETA into pizza tracker 2024-06-23 09:31:16 +00:00
Mononaut
f67ae10684 Integrate multi-pool ETA into transaction page 2024-06-23 09:30:02 +00:00
Mononaut
e11ce14f81 hashrate is a number not a string 2024-06-23 09:30:02 +00:00
Mononaut
833418514e Multi-pool ETA calculation 2024-06-23 09:30:01 +00:00
softsimon
6277813414 Merge pull request #5193 from mempool/mononaut/address-table-wrapping
Refactor address table to improve cell wrapping
2024-06-23 18:21:24 +09:00
softsimon
6936b97ba6 Merge pull request #5194 from mempool/simon/fix-stripped-mempool-transactions
Fix mempool transactions being stripped
2024-06-23 17:58:46 +09:00
softsimon
4dfabaf165 Fix mempool transactions being stripped
fixes #5150
2024-06-23 17:50:13 +09:00
softsimon
06f60df4cf Fix tx page crash when accelerationHistory errors 2024-06-23 15:38:53 +09:00
Mononaut
29a8f6a09e Refactor address table to improve cell wrapping 2024-06-23 03:17:33 +00:00
wiz
9394572ec3 Merge pull request #5190 from mempool/simon/address-page-updates
Address page ux updates
2024-06-22 17:53:00 +09:00
softsimon
8e521a2376 Add "confirmed" 2024-06-22 17:52:31 +09:00
softsimon
b227767fee Address page ux updates 2024-06-22 17:34:27 +09:00
natsoni
1c1c93abfc Fix websocket network change handling 2024-06-22 17:28:08 +09:00
softsimon
ec7c691044 Merge pull request #5189 from mempool/simon/twitter-to-x
Twitter -> X
2024-06-22 16:23:15 +09:00
softsimon
92e6df1295 Twitter -> X 2024-06-22 16:21:55 +09:00
softsimon
8b0015b3ff Merge pull request #5153 from mempool/natsoni/address-history-chart-usd
Add USD to address balance history chart
2024-06-22 16:11:56 +09:00
softsimon
19ea077fe5 Merge branch 'master' into natsoni/address-history-chart-usd 2024-06-22 15:54:31 +09:00
softsimon
16502332fd Merge pull request #5188 from mempool/natsoni/address-page-skeleton
Adapt address page skeleton
2024-06-22 15:53:37 +09:00
natsoni
7f2987f250 address page: adapt skeletons 2024-06-22 15:27:29 +09:00
natsoni
25e9741fc2 Set same start time for BTC and USD lines 2024-06-22 15:01:42 +09:00
softsimon
5be66f0b05 Merge pull request #5184 from mempool/mononaut/incoming-tx-scale
Always show clearing rate line on incoming tx chart
2024-06-22 14:57:31 +09:00
softsimon
a517c6c711 Prevent websocket reconnect on custom root instances 2024-06-22 14:55:59 +09:00
natsoni
43f35837da Merge branch 'master' into natsoni/address-history-chart-usd 2024-06-22 14:36:56 +09:00
softsimon
f9101b381b Merge pull request #5186 from mempool/mononaut/fix-hardcoded-median-weight
Fix hardcoded median weight units in calcEffectiveFeeStatistics
2024-06-22 14:18:58 +09:00
softsimon
6b84dc2be4 Merge branch 'master' into mononaut/fix-hardcoded-median-weight 2024-06-22 14:13:41 +09:00
softsimon
8082e1d1cf Merge pull request #5185 from mempool/mononaut/rust-gbt-block-size
configurable block size & count in rust gbt
2024-06-22 14:11:44 +09:00
Mononaut
36bc1db195 Fix hardcoded median weight units in calcEffectiveFeeStatistics 2024-06-22 04:38:06 +00:00
Mononaut
fa9a8bdba8 rust gbt restore 4kWU reserve 2024-06-22 04:30:36 +00:00
Mononaut
b44b790e28 configurable block size & count in rust gbt 2024-06-22 04:10:32 +00:00
softsimon
cf8d179925 Merge pull request #5176 from mempool/mononaut/fix-monitoring-layout
Fix monitoring table layout & text wrapping
2024-06-22 12:58:53 +09:00
softsimon
32db01d353 Merge pull request #5183 from mempool/simon/fix-invalid-json-response-missing-da
Missing difficulty adjustment causes invalid json response
2024-06-22 12:53:37 +09:00
Mononaut
7c806b4b23 Always show clearing rate line on incoming tx chart 2024-06-22 01:58:46 +00:00
softsimon
c581be0e97 Missing da causes invalid json 2024-06-22 10:52:01 +09:00
softsimon
e1e4e79b68 Merge pull request #5182 from mempool/simon/goggles-unit-tests
Unit tests: nonstandard
2024-06-22 09:54:37 +09:00
softsimon
246ca593bb Merge branch 'master' into simon/goggles-unit-tests 2024-06-22 09:43:35 +09:00
softsimon
136af78147 Merge pull request #5160 from mempool/mononaut/fix-nonstandard-label-bug
Fix incorrect non-standard label on reserved segwit output types
2024-06-22 09:32:06 +09:00
softsimon
da1ad1c316 Unit tests: nonstandard 2024-06-22 09:31:24 +09:00
softsimon
2e893e0aea adding missing }, to proxy conf 2024-06-22 08:21:56 +09:00
softsimon
b41382dfee Local dev accelerations proxy 2024-06-22 08:20:36 +09:00
softsimon
8d66374374 Merge pull request #5156 from mempool/simon/default-frontend-network-setting
Root frontend network setting
2024-06-22 07:56:13 +09:00
softsimon
c00d2f3763 Hack networkMatches 2024-06-21 19:32:25 +09:00
softsimon
e7cba13704 Add new frontend configs to docker 2024-06-21 19:09:35 +09:00
softsimon
55598e7974 Remove space between plus and amount 2024-06-21 18:38:37 +09:00
softsimon
bf81cc5ba9 Merge pull request #5159 from mempool/mononaut/handle-services-failures
Handle services backend failures in block component
2024-06-21 13:44:39 +09:00
Mononaut
c5b12e3bc3 split overview subscriptions in block component 2024-06-21 11:57:00 +09:00
softsimon
762c5aa718 Merge pull request #5169 from mempool/mononaut/core-gettxsforblock
Implement $getTxsForBlock for Core backends
2024-06-21 10:10:45 +09:00
softsimon
e95e64a443 Merge branch 'master' into mononaut/core-gettxsforblock 2024-06-21 08:03:34 +09:00
softsimon
d10fdaad46 Merge pull request #5177 from mempool/simon/deprecate-unique-pool-id
Deprecate pool_unique_id, fixing accelerations sync
2024-06-21 03:31:13 +09:00
Mononaut
5b554852bb Recover from esplora failover after a reorg to lower height 2024-06-20 14:29:35 +00:00
softsimon
ff8fb3b24f Merge pull request #5151 from mempool/mononaut/address-redesign-phase-1
Address page redesign phase 1
2024-06-20 17:51:48 +09:00
softsimon
1219526e2d Disabling liquid test and fixing liquid overflow 2024-06-20 17:43:54 +09:00
softsimon
85006a5bec some -> includes 2024-06-20 14:36:58 +09:00
softsimon
82e2f46eba Merge pull request #5134 from mempool/natsoni/improve-conversions-price-service
Improve price conversions fetching from free API
2024-06-20 14:15:07 +09:00
softsimon
0719b20110 Deprecate pool_unique_id 2024-06-20 12:22:54 +09:00
Mononaut
25b510359f Fix monitoring table layout & text wrapping 2024-06-20 03:09:54 +00:00
softsimon
02eb633d89 Merge pull request #5171 from mempool/nymkappa/fix-accel-dashboard-many-pending
[accelerator] always show last 6 completed accelerations in accel dashboard
2024-06-20 02:16:28 +09:00
nymkappa
522a473213 [accelerator] always show last 6 completed accelerations in accel dashboard 2024-06-19 17:32:16 +09:00
softsimon
bc583979c5 Merge pull request #5158 from mempool/dependabot/npm_and_yarn/frontend/braces-3.0.3
Bump braces from 3.0.2 to 3.0.3 in /frontend
2024-06-19 16:04:44 +09:00
softsimon
3222e0efd2 Merge pull request #5170 from mackalex/fix-grammatical-errors
Small grammatical or typo fixes in backend README
2024-06-19 16:03:45 +09:00
Alex Makoviecki
f720c90c03 Small grammatical or typo fixes in backend README 2024-06-18 22:36:32 -07:00
Mononaut
1bf5047377 Implement $getTxsForBlock for Core backends 2024-06-19 03:15:23 +00:00
dependabot[bot]
bdeac877d2 Bump braces from 3.0.2 to 3.0.3 in /frontend
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 03:01:39 +00:00
dependabot[bot]
1bcacf53be Bump braces from 3.0.2 to 3.0.3 in /unfurler
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 03:01:16 +00:00
softsimon
0c8d9daaec Merge pull request #5165 from mempool/dependabot/npm_and_yarn/backend/ws-8.17.1
Bump ws from 8.17.0 to 8.17.1 in /backend
2024-06-19 12:00:46 +09:00
softsimon
307d3627a0 Merge pull request #5164 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.12.0
Bump cypress from 13.11.0 to 13.12.0 in /frontend
2024-06-19 12:00:30 +09:00
dependabot[bot]
db04c4663e Bump ws from 8.17.0 to 8.17.1 in /backend
Bumps [ws](https://github.com/websockets/ws) from 8.17.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.17.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 02:27:59 +00:00
dependabot[bot]
a0a6a0da4f Bump cypress from 13.11.0 to 13.12.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.11.0 to 13.12.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.11.0...v13.12.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 02:21:54 +00:00
natsoni
26968605cc Display both BTC and USD in address history graph 2024-06-18 11:27:12 +02:00
wiz
ccd8412e6a Merge pull request #5157 from mempool/nymkappa/grumpy
[about page] use grumpy guy instead of boring placeholder
2024-06-17 11:12:26 +09:00
Mononaut
272a2c8441 Fix incorrect non-standard label on reserved segwit output types 2024-06-16 22:25:40 +00:00
softsimon
2ee656a176 Renaming default to root network 2024-06-16 10:50:31 +02:00
Mononaut
5cecd9f8a7 Address page bigger QR button on mobile 2024-06-15 21:02:32 +00:00
softsimon
c0ec9f70c3 Fix dropdown visibility when using only 1 random network 2024-06-15 13:00:23 +02:00
nymkappa
84dae82e90 [about page] use grumpy guy instead of boring placeholder 2024-06-15 18:50:27 +09:00
softsimon
9dbf3b54fb Electrs network routing fix 2024-06-15 05:18:35 +02:00
softsimon
ce46aae8cc Default frontend network setting 2024-06-15 00:22:33 +02:00
Mononaut
fb621f9812 Address redesign liquid & layout fixes 2024-06-14 15:51:00 +00:00
natsoni
2156924d7e Prevent address txs widget to send too many price requests 2024-06-12 20:02:54 +02:00
natsoni
60a30aaede Allow to open transaction in new tab/page when click on address graph 2024-06-12 20:01:48 +02:00
Mononaut
7dfdb5553e Address & script parsing refactor 2024-06-12 17:28:43 +00:00
natsoni
824bf5fc63 Fix price fetching causing race condition 2024-06-12 16:57:19 +02:00
natsoni
2b44055fc7 Add support for zooming in address balance graph 2024-06-12 13:17:39 +02:00
natsoni
7bef8653b1 Add support for USD in address history graph 2024-06-12 11:47:57 +02:00
Mononaut
3b419be341 Address details pending -> unconfirmed 2024-06-11 20:51:17 +00:00
Mononaut
331b54fe89 Address mouseover QR code 2024-06-10 23:22:10 +00:00
Mononaut
9514bb703b Redesign top of address page 2024-06-10 23:04:37 +00:00
Mononaut
746a045c48 Refactor address page component with AddressStats class 2024-06-10 22:03:07 +00:00
softsimon
684ad9f0e6 Merge pull request #5062 from mempool/mononaut/configurable-tip-monitoring
Configurable threshold for esplora tip check
2024-06-10 00:52:50 +04:00
softsimon
24b5d4e971 Fix docker default value 2024-06-10 00:52:39 +04:00
softsimon
fda40cad48 Fix trailing comma 2024-06-10 00:47:40 +04:00
softsimon
2ce4b5604e Merge pull request #5130 from mempool/dependabot/docker/docker/frontend/node-20.14.0-buster-slim
Bump node from 20.13.1-buster-slim to 20.14.0-buster-slim in /docker/frontend
2024-06-10 00:16:42 +04:00
softsimon
fb660e8477 Merge pull request #5129 from mempool/dependabot/docker/docker/frontend/nginx-1.27.0-alpine
Bump nginx from 1.26.0-alpine to 1.27.0-alpine in /docker/frontend
2024-06-10 00:16:34 +04:00
softsimon
621def712d Merge pull request #5128 from mempool/dependabot/docker/docker/backend/node-20.14.0-buster-slim
Bump node from 20.13.1-buster-slim to 20.14.0-buster-slim in /docker/backend
2024-06-10 00:16:21 +04:00
softsimon
8382a27a7c Merge pull request #5149 from mempool/mononaut/accurate-timestamps-hover
Accurate timestamps on hover
2024-06-10 00:16:07 +04:00
softsimon
b7d96a2a26 Merge pull request #5145 from mempool/natsoni/tapscript-toggle-show-more
Refactor "show all" toggle for long witnesses and witness scripts
2024-06-10 00:11:26 +04:00
Mononaut
3149199c8a Accurate timestamps on hover 2024-06-08 23:28:44 +00:00
softsimon
0c3ef4eabc Merge pull request #5139 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.11.0
Bump cypress from 13.10.0 to 13.11.0 in /frontend
2024-06-08 19:12:02 +04:00
wiz
fffcb5038f Merge pull request #5136 from mempool/mononaut/research
research footer link
2024-06-07 11:49:51 +09:00
natsoni
77d42bfdbb Don't render full input witness if user does not press "show all" 2024-06-06 17:53:20 +02:00
natsoni
f840ac951b Add show all toggle for redeem scripts 2024-06-06 11:43:21 +02:00
wiz
22a48efd19 Merge pull request #5141 from mempool/nymkappa/liquid-fix
[liquid] don't fetch pools
2024-06-05 15:31:44 +09:00
nymkappa
fba3f7ec1c [liquid] don't fetch pools 2024-06-05 08:28:01 +02:00
wiz
6b3005c49d Merge pull request #5125 from mempool/mononaut/recent-address-chart
Make address chart prefer "recent" mode by default
2024-06-05 14:22:00 +09:00
wiz
17132ff047 Merge pull request #5120 from mempool/mononaut/multi-pool-eta
Multi-pool ETA
2024-06-05 14:21:34 +09:00
wiz
355fe58b43 Merge pull request #5137 from mempool/mononaut/pizza-replacement
Pizza tracker RBF support
2024-06-05 14:20:51 +09:00
wiz
604b0ba3e6 Merge pull request #5135 from mempool/mononaut/research-images
research images
2024-06-05 14:19:34 +09:00
dependabot[bot]
d016838356 Bump cypress from 13.10.0 to 13.11.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.10.0 to 13.11.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.10.0...v13.11.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-05 02:38:18 +00:00
Mononaut
f77582250f Pizza tracker rbf tree traversal to find mined tx 2024-06-04 23:02:13 +00:00
Mononaut
976e505445 Pizza tracker handle RBF replacements 2024-06-04 22:45:43 +00:00
Mononaut
42c60fd991 Defer db access to fix failing tests 2024-06-04 20:57:40 +00:00
Mononaut
9a838c7269 Use estimated acceleration positions in frontend 2024-06-04 20:40:41 +00:00
Mononaut
f31b28251c Estimate accelerated positions in partner mempools 2024-06-04 20:40:40 +00:00
Mononaut
ced1595d70 research footer link 2024-06-04 15:50:13 +00:00
Mononaut
0b0109d821 Research unfurler preview image 2024-06-04 15:25:04 +00:00
Mononaut
992da1e5d2 Research logo 2024-06-04 15:09:45 +00:00
natsoni
25c0eb62b2 More robust price service 2024-06-04 10:58:04 +02:00
dependabot[bot]
9d4bbe9317 Bump node in /docker/frontend
Bumps node from 20.13.1-buster-slim to 20.14.0-buster-slim.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 02:53:03 +00:00
dependabot[bot]
5575798cb6 Bump nginx from 1.26.0-alpine to 1.27.0-alpine in /docker/frontend
Bumps nginx from 1.26.0-alpine to 1.27.0-alpine.

---
updated-dependencies:
- dependency-name: nginx
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 02:52:57 +00:00
dependabot[bot]
57cc53b64e Bump node in /docker/backend
Bumps node from 20.13.1-buster-slim to 20.14.0-buster-slim.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 02:17:00 +00:00
Mononaut
37725bb341 Make address graph prefer "recent" mode by default 2024-05-31 17:20:07 +00:00
Hans ❤️ Crypto
0e37e85af6 Create hans-crypto.txt 2024-05-21 12:35:20 +02:00
Hans ❤️ Crypto
4b3123b5ae Remove reference to bisq in unfurler
not needed anymore
2024-05-21 09:00:05 +02:00
nymkappa
0605e80d89 Merge branch 'master' into nymkappa/prepaid-update-price 2024-05-19 08:10:23 +02:00
Mononaut
568084e143 Configurable threshold for esplora tip check 2024-05-12 00:35:25 +00:00
wiz
6accf8420f Merge branch 'master' into nymkappa/prepaid-update-price 2024-04-25 02:24:42 +09:00
orangesurf
6e2c0bac43 Update accelerate-checkout.component.ts
Analysis suggests 1.5 would be a good starting point

https://gist.github.com/orangesurf/5f69da2ffbdd1b737be53789e1783b03
2024-04-22 20:18:40 +02:00
nymkappa
9363004252 [accelerator] change default bid prepaid 2024-04-22 08:08:03 +02:00
597 changed files with 93122 additions and 42108 deletions

View File

@@ -9,7 +9,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
node: ["20", "21"]
node: ["20", "22"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
@@ -160,7 +160,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy:
matrix:
node: ["20", "21"]
node: ["20", "22"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
@@ -251,17 +251,7 @@ jobs:
strategy:
fail-fast: false
matrix:
module: ["mempool", "liquid"]
include:
- module: "mempool"
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet4/*.spec.ts
- module: "liquid"
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
module: ["mempool", "liquid", "testnet4"]
name: E2E tests for ${{ matrix.module }}
steps:
@@ -273,7 +263,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 22
cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
@@ -310,8 +300,10 @@ jobs:
- name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
@@ -322,7 +314,9 @@ jobs:
wait-on-timeout: 120
record: true
parallel: true
spec: ${{ matrix.spec }}
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
@@ -332,6 +326,56 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# liquid
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'liquid' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# testnet
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'testnet4' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:mempool
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/testnet4/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
CYPRESS_REROUTE_TESTNET: true
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
validate_docker_json:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
@@ -359,4 +403,4 @@ jobs:
- name: Validate JSON syntax
run: |
cat mempool-config.json | jq
working-directory: docker/docker/backend
working-directory: docker/docker/backend

2
.nvmrc
View File

@@ -1 +1 @@
v20.8.0
v22.13.0

12
LICENSE
View File

@@ -1,5 +1,5 @@
The Mempool Open Source Project®
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
@@ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project.
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
of Mempool Space K.K in Japan, the United States, and/or other countries.
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
registered trademarks or trademarks of Mempool Space K.K in Japan,
the United States, and/or other countries.
See our full Trademark Policy and Guidelines for more details, published on
<https://mempool.space/trademark-policy>.

View File

@@ -34,6 +34,7 @@
"quotes": [1, "single", { "allowTemplateLiterals": true }],
"semi": 1,
"curly": [1, "all"],
"eqeqeq": 1
"eqeqeq": 1,
"no-trailing-spaces": 1
}
}

View File

@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build
_Make sure to use Node.js 16.10 and npm 7._
_Make sure to use Node.js 20.x and npm 9.x or newer_
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
@@ -181,7 +181,7 @@ Create a new wallet, if needed:
bitcoin-cli -regtest createwallet test
```
Load wallet (this command may take a while if you have lot of UTXOs):
Load wallet (this command may take a while if you have a lot of UTXOs):
```
bitcoin-cli -regtest loadwallet test
```
@@ -229,13 +229,13 @@ Generate block at regular interval (every 10 seconds in this example):
### Mining pools update
By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` is set to `false`).
By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` is set to `false`).
To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks.
You can enabled the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`.
You can enable the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` to `true` in your `mempool-config.json`.
When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionaly, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed.
When a `coinbase tag` or `coinbase address` change is detected, pool assignments for all relevant blocks (tagged to that pool or the `unknown` mining pool, starting from height 130635) are updated using the new criteria.
### Re-index tables

View File

@@ -24,11 +24,12 @@
"EXTERNAL_RETRY_INTERVAL": 0,
"USER_AGENT": "mempool",
"STDOUT_LOG_MIN_PRIORITY": "debug",
"AUTOMATIC_BLOCK_REINDEXING": false,
"AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": false,
"RUST_GBT": false,
"RUST_GBT": true,
"LIMIT_GBT": false,
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6,
@@ -45,7 +46,8 @@
"PASSWORD": "mempool",
"TIMEOUT": 60000,
"COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
"COOKIE_PATH": "/path/to/bitcoin/.cookie",
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
@@ -59,7 +61,8 @@
"RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",
@@ -152,6 +155,10 @@
"API": "https://mempool.space/api/v1/services",
"ACCELERATIONS": false
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
},
"FIAT_PRICE": {
"ENABLED": true,
"PAID": false,

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "3.0.0-dev",
"version": "3.1.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -39,24 +39,24 @@
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
},
"dependencies": {
"@babel/core": "^7.24.0",
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.7.2",
"axios": "1.7.2",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.19.2",
"express": "~4.21.1",
"maxmind": "~4.3.11",
"mysql2": "~3.10.0",
"mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"redis": "^4.7.0",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.17.0"
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.24.0",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",

View File

@@ -10,7 +10,7 @@
"UNIX_SOCKET_PATH": "/mempool/socket/mempool-bitcoin-mainnet",
"SPAWN_CLUSTER_PROCS": 2,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"AUTOMATIC_BLOCK_REINDEXING": false,
"AUTOMATIC_POOLS_UPDATE": false,
"POLL_RATE_MS": 3,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CACHE_ENABLED": true,
@@ -28,6 +28,7 @@
"INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": true,
"RUST_GBT": false,
"LIMIT_GBT": false,
@@ -46,7 +47,8 @@
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000,
"COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@@ -60,7 +62,8 @@
"RETRY_UNIX_SOCKET_AFTER": 888,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
@@ -148,5 +151,9 @@
"ENABLED": true,
"PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
}
}

View File

@@ -1,24 +1,40 @@
import { Common } from '../../api/common';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
const randomTransactions = require('./test-data/transactions-random.json');
const replacedTransactions = require('./test-data/transactions-replaced.json');
const rbfTransactions = require('./test-data/transactions-rbfs.json');
const nonStandardTransactions = require('./test-data/non-standard-txs.json');
describe('Mempool Utils', () => {
test('should detect RBF transactions with fast method', () => {
describe('Common', () => {
describe('RBF', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
test('should detect RBF transactions with fast method', () => {
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
});
test('should detect RBF transactions with scalable method', () => {
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
});
});
test.only('should detect RBF transactions with scalable method', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
describe('Mempool Goggles', () => {
test('should detect nonstandard transactions', () => {
nonStandardTransactions.forEach((tx) => {
expect(Common.isNonStandard(tx)).toEqual(true);
});
});
test('should not misclassify as nonstandard transactions', () => {
randomTransactions.forEach((tx) => {
expect(Common.isNonStandard(tx)).toEqual(false);
});
});
});
});

View File

@@ -0,0 +1,52 @@
[
{
"txid": "50136231cb7eeeffb17fc41d1cca213426abe5bf3760e3d6421cad0c0edad367",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "c7f86fb7b830124057475b282809f3474ef3565daa3de0b599980fb9e84ab019",
"vout": 4217,
"prevout": {
"scriptpubkey": "001466197b5eadd8067ec194a457e1044b6d1fbdd3b3",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 66197b5eadd8067ec194a457e1044b6d1fbdd3b3",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qvcvhkh4dmqr8asv553t7zpztd50mm5ang4na33",
"value": 106
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3043021f2af6060a142c6cfd7428adad6a50745d2424813d7ced5c0bbcca85e70de1be022021440ca1c8c3ed49ecd1b64dca6911adcd430c5d3dd60d77ffe0072953999f5b01",
"02ead5c34e3d2c506574b562f857576e11380b6ba15d9f0ad7b7303fdaa9c1513d"
],
"is_coinbase": false,
"sequence": 4294967295
}
],
"vout": [
{
"scriptpubkey": "6a023a29",
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_2 3a29",
"scriptpubkey_type": "op_return",
"value": 0
},
{
"scriptpubkey": "6a036d7648",
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_3 6d7648",
"scriptpubkey_type": "op_return",
"value": 0
}
],
"size": 186,
"weight": 420,
"sigops": 1,
"fee": 106,
"status": {
"confirmed": true,
"block_height": 836361,
"block_hash": "0000000000000000000341cc26cda4af82cd25f7063c448772228cbf2836915b",
"block_time": 1711448028
}
}
]

View File

@@ -273,5 +273,328 @@
},
"bestDescendant": null,
"cpfpChecked": true
},
{
"txid": "20b984492b5264162a4c92c9a34bc7fa08b67d669de7b4c5982ad3cb28aaecf6",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "3adda6afd547193793c248e667c2b7dbf26d705003de65e3a25e5be698286aef",
"vout": 2,
"prevout": {
"scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6",
"value": 27619
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402205d7f1e0d928982645c2bcc4c730c4545c382d6520c2a14eebc71594702cd06b302200511d452ce51c79017536f50acb115eefe7c04506ad12b9307d2b5d56b999beb01",
"03716cb4f0430fe69c596a12c6680c55803150645989b406772838d548cde7cca5"
],
"is_coinbase": false,
"sequence": 4294967295
}
],
"vout": [
{
"scriptpubkey": "6a5d0614c0a2331441",
"scriptpubkey_asm": "OP_RETURN OP_PUSHNUM_13 OP_PUSHBYTES_6 14c0a2331441",
"scriptpubkey_type": "op_return",
"value": 0
},
{
"scriptpubkey": "5114d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_20 d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c",
"scriptpubkey_type": "unknown",
"scriptpubkey_address": "bc1p6uwxc048hflxaerh5zlastpqc7ye0zpvq7gq2a",
"value": 546
},
{
"scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6",
"value": 23073
}
],
"size": 240,
"weight": 633,
"sigops": 1,
"fee": 4000,
"status": {
"confirmed": true,
"block_height": 848136,
"block_hash": "00000000000000000002c69c7a3010fcd596c0c7451c23e7cd1f5e19ebf8ee6d",
"block_time": 1718517071
}
},
{
"txid": "b10c0000004da5a9d1d9b4ae32e09f0b3e62d21a5cce5428d4ad714fb444eb5d",
"version": 1,
"locktime": 1231006505,
"vin": [
{
"txid": "d46a24962c1d7bd6e87d80570c6a53413eaf30d7fde7f52347f13645ae53969b",
"vout": 0,
"prevout": {
"scriptpubkey": "41049434a2dd7c5b82df88f578f8d7fd14e8d36513aaa9c003eb5bd6cb56065e44b7e0227139e8a8e68e7de0a4ed32b8c90edc9673b8a7ea541b52f2a22196f7b8cfac",
"scriptpubkey_asm": "OP_PUSHBYTES_65 049434a2dd7c5b82df88f578f8d7fd14e8d36513aaa9c003eb5bd6cb56065e44b7e0227139e8a8e68e7de0a4ed32b8c90edc9673b8a7ea541b52f2a22196f7b8cf OP_CHECKSIG",
"scriptpubkey_type": "p2pk",
"value": 6102
},
"scriptsig": "473044022004f027ae0b19bb7a7aa8fcdf135f1da769d087342020359ef4099a9f0f0ba4ec02206a83a9b78df3fed89a3b6052e69963e1fb08d8f6d17d945e43b51b5214aa41e601",
"scriptsig_asm": "OP_PUSHBYTES_71 3044022004f027ae0b19bb7a7aa8fcdf135f1da769d087342020359ef4099a9f0f0ba4ec02206a83a9b78df3fed89a3b6052e69963e1fb08d8f6d17d945e43b51b5214aa41e601",
"is_coinbase": false,
"sequence": 20090103
},
{
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914bbb1f7d0f7e15ac088af9bafe25aaac1a59832d088ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 bbb1f7d0f7e15ac088af9bafe25aaac1a59832d0 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1J7SZJry7CX4zWdH3P8E8UJjZrhcLEjJ39",
"value": 1913
},
"scriptsig": "46304302204dc2939be89ab6626457fff40aec2cc4e6213e64bcb4d2c43bf6b49358ff638c021f33d2f8fdf6d54a2c82bb7cddc62becc2cbbaca6fd7f3ec927ea975f29ad8510221028b98707adfd6f468d56c1a6067a6f0c7fef43afbacad45384017f8be93a18d40",
"scriptsig_asm": "OP_PUSHBYTES_70 304302204dc2939be89ab6626457fff40aec2cc4e6213e64bcb4d2c43bf6b49358ff638c021f33d2f8fdf6d54a2c82bb7cddc62becc2cbbaca6fd7f3ec927ea975f29ad85102 OP_PUSHBYTES_33 028b98707adfd6f468d56c1a6067a6f0c7fef43afbacad45384017f8be93a18d40",
"is_coinbase": false,
"sequence": 20081031
},
{
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
"vout": 1,
"prevout": {
"scriptpubkey": "52210304e708d258a632ffb128a62ecf5eebd1904e505497d031619513afc8bca7858f2102b9dc03f1133e7cbc7eb311631acc2dbda908fb0f0fae095da2f4dd427f51308a4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f53ae",
"scriptpubkey_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 0304e708d258a632ffb128a62ecf5eebd1904e505497d031619513afc8bca7858f OP_PUSHBYTES_33 02b9dc03f1133e7cbc7eb311631acc2dbda908fb0f0fae095da2f4dd427f51308a OP_PUSHBYTES_65 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_PUSHNUM_3 OP_CHECKMULTISIG",
"scriptpubkey_type": "multisig",
"value": 1971
},
"scriptsig": "00453042021e4f6ff73d7b304a5cbf3bb7738abb5f81a4af6335962134ce27a1cc45fec702201b95e3acb7db93257b20651cdcb79af66bf0bb86a8ae5b4e0a5df4e3f86787e2033b303802153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021f34793e2878497561e7616291ebdda3024b681cdacc8b863b5b0804cd30c2a481",
"scriptsig_asm": "OP_0 OP_PUSHBYTES_69 3042021e4f6ff73d7b304a5cbf3bb7738abb5f81a4af6335962134ce27a1cc45fec702201b95e3acb7db93257b20651cdcb79af66bf0bb86a8ae5b4e0a5df4e3f86787e203 OP_PUSHBYTES_59 303802153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021f34793e2878497561e7616291ebdda3024b681cdacc8b863b5b0804cd30c2a481",
"is_coinbase": false,
"sequence": 19750504
},
{
"txid": "45e1cb33599acb071810ccc801b71bd7610865f5b899492946ab1bfbcb61cad6",
"vout": 0,
"prevout": {
"scriptpubkey": "a91419f0b86f61606c6eb51b217698ca7e8bff1e398b87",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 19f0b86f61606c6eb51b217698ca7e8bff1e398b OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "344BBtYkhaCXgA7oYSXASUfh4bFieiponG",
"value": 2140
},
"scriptsig": "00443041021d1313459a48bd1d0628eec635495f793e970729684394f9b814d2b24012022050be6d9918444e283da0136884f8311ec465d0fed2f8d24b75a8485ebdc13aea013a303702153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021e78644ba72eab69fefb5fe50700671bfb91dda699f72ffbb325edc6a3c4ef8239303602153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021d2c2db104e70720c39af43b6ba3edd930c26e0818aa59ff9c886281d8ba834ced532103e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc2103cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd421027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a34104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c55ae",
"scriptsig_asm": "OP_0 OP_PUSHBYTES_68 3041021d1313459a48bd1d0628eec635495f793e970729684394f9b814d2b24012022050be6d9918444e283da0136884f8311ec465d0fed2f8d24b75a8485ebdc13aea01 OP_PUSHBYTES_58 303702153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021e78644ba72eab69fefb5fe50700671bfb91dda699f72ffbb325edc6a3c4ef82 OP_PUSHBYTES_57 303602153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021d2c2db104e70720c39af43b6ba3edd930c26e0818aa59ff9c886281d8ba83 OP_PUSHDATA1 532103e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc2103cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd421027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a34104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c55ae",
"is_coinbase": false,
"sequence": 16,
"inner_redeemscript_asm": "OP_PUSHNUM_3 OP_PUSHBYTES_33 03e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc OP_PUSHBYTES_33 03cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd4 OP_PUSHBYTES_33 027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906 OP_PUSHBYTES_65 0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 OP_PUSHBYTES_65 04ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c OP_PUSHNUM_5 OP_CHECKMULTISIG"
},
{
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
"vout": 2,
"prevout": {
"scriptpubkey": "a9143b13a1f71c20c799d86bb624b3898c826d6c82da87",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 3b13a1f71c20c799d86bb624b3898c826d6c82da OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "375PJxsKRtAq4WoS6u82jvgZW94R8Wx3iH",
"value": 5139
},
"scriptsig": "1600149b27f072e4b972927c445d1946162a550b0914d8",
"scriptsig_asm": "OP_PUSHBYTES_22 00149b27f072e4b972927c445d1946162a550b0914d8",
"witness": [
"3040021c23902a01d4c5cff2c33c8bdb778a5aadea78a9a0d6d4db60aaa0fba1022069237d9dbf2db8cff9c260ba71250493682d01a746f4a45c5c7ea386e56d2bc902",
"0240187acd3e2fd3d8e1acffefa85907b6550730c24f78dfd3301c829fc4daf3cc"
],
"is_coinbase": false,
"sequence": 141,
"inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_20 9b27f072e4b972927c445d1946162a550b0914d8"
},
{
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
"vout": 3,
"prevout": {
"scriptpubkey": "a914a3c0698f2300c7b2e8107d4c9c988e642110039087",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 a3c0698f2300c7b2e8107d4c9c988e6421100390 OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "3GcrZrbUuvE4UtUdSbKTXcRnTqmfMdyMAC",
"value": 3220
},
"scriptsig": "220020a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75",
"scriptsig_asm": "OP_PUSHBYTES_34 0020a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75",
"witness": [
"303f021c65aee6696e80be6e14545cfd64b44f17b0514c150eefdb090c0f0bd9021f3fef4aa95c252a225622aba99e4d5af5a6fe40d177acd593e64cf2f8557ccc03",
"03b55c6f0749e0f3e2caeca05f68e3699f1b3c62a550730f704985a6a9aae437a1",
"76a914db865fd920959506111079995f1e4017b489bfe38763ac6721024d560f7f5d28aae5e1a8aa2b7ba615d7fc48e4ea27e5d27336e6a8f5fa0f5c8c7c820120876475527c2103443e8834fa7d79d7b5e95e0e9d0847f6b03ac3ea977979858b4104947fca87ca52ae67a91446c3747322b220fdb925c9802f0e949c1feab99988ac6868"
],
"is_coinbase": false,
"sequence": 3735928559,
"inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_32 a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75",
"inner_witnessscript_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 db865fd920959506111079995f1e4017b489bfe3 OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 024d560f7f5d28aae5e1a8aa2b7ba615d7fc48e4ea27e5d27336e6a8f5fa0f5c8c OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 03443e8834fa7d79d7b5e95e0e9d0847f6b03ac3ea977979858b4104947fca87ca OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 46c3747322b220fdb925c9802f0e949c1feab999 OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF OP_ENDIF"
},
{
"txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3",
"vout": 4,
"prevout": {
"scriptpubkey": "0014c0ca6e754e65d3ba59112d7abc33e500c00ecfa7",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 c0ca6e754e65d3ba59112d7abc33e500c00ecfa7",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qcr9xua2wvhfm5kg394atcvl9qrqqana8rrmy8h",
"value": 17144
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"303e021c11f60486afd0f5d6573603fb2076ef2f676455b92ada257d2f25558a021e317719c946f951d49bf4df4285a618629cd9e554fcbf787c319a0c4dd22601",
"032467f24cc31664f0cf34ff8d5cbb590888ddc1dcfec724a32ae3dd5338b8508e"
],
"is_coinbase": false,
"sequence": 21000000
},
{
"txid": "637db3928a8fb1b22b81f92dc738ee7637e5b172d650363d0b327429578bd001",
"vout": 0,
"prevout": {
"scriptpubkey": "0020a9530a167fcada672c142ee636dcd171796e69ef8e37aa1f77f35c58edd7a357",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 a9530a167fcada672c142ee636dcd171796e69ef8e37aa1f77f35c58edd7a357",
"scriptpubkey_type": "v0_p2wsh",
"scriptpubkey_address": "bc1q49fs59nletdxwtq59mnrdhx3w9uku6003cm658mh7dw93mwh5dts2w2kht",
"value": 8149
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"303d021c32f9454db85cb1a4ca63a9883d4347c5e13f3654e884ae44e9efa3c8021d62f07fe452c06b084bc3e09afd3aac4039136549a465533bc1ca66967902",
"01",
"632102fd6db4de50399b2aa086edb23f8e140bbc823d6651e024a0eb871288068789cd67012ab27521034134a2bb35c3f83dab2489d96160741888b8b5589bb694dea6e7bc24486e9c6f68ac"
],
"is_coinbase": false,
"sequence": 4190024921,
"inner_witnessscript_asm": "OP_IF OP_PUSHBYTES_33 02fd6db4de50399b2aa086edb23f8e140bbc823d6651e024a0eb871288068789cd OP_ELSE OP_PUSHBYTES_1 2a OP_CSV OP_DROP OP_PUSHBYTES_33 034134a2bb35c3f83dab2489d96160741888b8b5589bb694dea6e7bc24486e9c6f OP_ENDIF OP_CHECKSIG"
},
{
"txid": "0020db02df125062ebae5bacd189ebff22577b2817c1872be79a0d3ba3982c41",
"vout": 0,
"prevout": {
"scriptpubkey": "512071212ded0ff4c9b1b0c505d8012772e2dbe98a3cae7168377b950fb6b866a849",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 71212ded0ff4c9b1b0c505d8012772e2dbe98a3cae7168377b950fb6b866a849",
"scriptpubkey_type": "v1_p2tr",
"scriptpubkey_address": "bc1pwysjmmg07nymrvx9qhvqzfmjutd7nz3u4ecksdmmj58mdwrx4pysq6m68g",
"value": 9001
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"d822f203827852998cad370232e8c57294540a5da51107fa26cf466bdd2b8b0b3d161999cc80aed8de7386a2bd5d5313aea159a231cc26fa53aaa702b7fa21ed"
],
"is_coinbase": false,
"sequence": 341
},
{
"txid": "795741ecf9c431b14b1c8d2dd017d3978fd4f6452e91edf416f31ef9971206b4",
"vout": 0,
"prevout": {
"scriptpubkey": "512089ac120a490eee88db5588112f95f88093284c814f07c3ad943a7faefba2271a",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 89ac120a490eee88db5588112f95f88093284c814f07c3ad943a7faefba2271a",
"scriptpubkey_type": "v1_p2tr",
"scriptpubkey_address": "bc1p3xkpyzjfpmhg3k643qgjl90cszfjsnypfuru8tv58fl6a7azyudqkcu66k",
"value": 19953
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"fe6eb715dceffefc067fdc787d250a9a9116682d216f6356ea38fc1f112bd74995faa90315e81981d2c2260b7eaca3c41a16b280362980f0d8faf4c05ebb82c5",
"e34ad0ad33885a473831f8ba8d9339123cb19d0e642e156d8e0d6e2ab2691aedb30e55a35637a806927225e1aa72223d41e59f92c6579b819e7d331a7ada9d2e01",
"2a4861fb4cb951c791bf6c93859ef65abccd90034f91b9b77abb918e13b6fce75d5fa3e2d2f6eeeae105315178c2cb9db2ef238fe89b282f691c06db43bc71ca02",
"fc97bb2be673c3bf388aaf58178ef14d354caf83c92aca8ef1831d619b8511e928f4f5fdea3962067b11e7cecfe094cd0f66a4ea9af9ec836d70d18f2b37df0281",
"a5781a0adaa80ab7f7f164172dd1a1cb127e523daa0d6949aba074a15c589f12dfb8183182afec9230cb7947b7422a4abc1bb78173550d66274ea19f6c9dd92c82",
"",
"",
"205f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1ac205f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3ba205ac8ff25ce63564963d1148b84627f614af1f3c77d7caa23adc61264fa5e4996ba20b210c83e6f5b3f866837112d023d9ae8da2a6412168d54968ab87860ab970690ba20d3ee3b7a8b8149122b3c886330b3241538ba4b935c4040f4a73ddab917241bc5ba20cdfabb9d0e5c8f09a83f19e36e100d8f5e882f1b60aa60dacd9e6d072c117bc0ba20aab038c238e95fb54cdd0a6705dc1b1f8d135a9e9b20ab9c7ff96eef0e9bf545ba559c",
"c0b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f5534a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bf4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e166f7cf9580f1c2dfb3c4d5d043cdbb128c640e3f20161245aa7372e9666168516a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48dd5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb46829a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713d29c9c0e8e4d2a9790922af73f0b8d51f0bd4bb19940d9cf910ead8fbe85bc9bbb41a757f405890fb0f5856228e23b715702d714d59bf2b1feb70d8b2b4e3e089fdbcf0ef9d8d00f66e47917f67cc5d78aec1ac786e2abb8d2facb4e4790aad6cc455ae816e6cdafdb58d54e35d4f46d860047458eacf1c7405dc634631c570d8d31992805518fd62daa3bdd2a5c4fd2cd3054c9b3dca1d78055e9528cff6adc8f907925d2ebe48765103e6845c06f1f2bb77c6adc1cc002865865eb5cfd5c1cb10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d4133e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac9879903637777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8fd456524104a6674693c29946543f8a0befccce5a352bda55ec8559fc630f5f37393096d97bfee8660f4100ffd61874d62f9a65de9fb6acf740c4c386990ef7373be398c4bdc43709db7398106609eea2a7841aaf3a4fa2000dc18184faa2a7eb5a2af5845a8d3796308ff9840e567b14cf6bb158ff26c999e6f9a1f5448f9aa"
],
"is_coinbase": false,
"sequence": 342,
"inner_witnessscript_asm": "OP_PUSHBYTES_32 5f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1 OP_CHECKSIG OP_PUSHBYTES_32 5f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3 OP_CHECKSIGADD OP_PUSHBYTES_32 5ac8ff25ce63564963d1148b84627f614af1f3c77d7caa23adc61264fa5e4996 OP_CHECKSIGADD OP_PUSHBYTES_32 b210c83e6f5b3f866837112d023d9ae8da2a6412168d54968ab87860ab970690 OP_CHECKSIGADD OP_PUSHBYTES_32 d3ee3b7a8b8149122b3c886330b3241538ba4b935c4040f4a73ddab917241bc5 OP_CHECKSIGADD OP_PUSHBYTES_32 cdfabb9d0e5c8f09a83f19e36e100d8f5e882f1b60aa60dacd9e6d072c117bc0 OP_CHECKSIGADD OP_PUSHBYTES_32 aab038c238e95fb54cdd0a6705dc1b1f8d135a9e9b20ab9c7ff96eef0e9bf545 OP_CHECKSIGADD OP_PUSHNUM_5 OP_NUMEQUAL"
}
],
"vout": [
{
"scriptpubkey": "210261542eb020b36c1da48e2e607b90a8c1f2ccdbd06eaf5fb4bb0d7cc34293d32aac",
"scriptpubkey_asm": "OP_PUSHBYTES_33 0261542eb020b36c1da48e2e607b90a8c1f2ccdbd06eaf5fb4bb0d7cc34293d32a OP_CHECKSIG",
"scriptpubkey_type": "p2pk",
"value": 576
},
{
"scriptpubkey": "76a9140240539af6c68431e4ce9cc5ef464f12c1741b3c88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 0240539af6c68431e4ce9cc5ef464f12c1741b3c OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1CuQsdrcgcmPvugo3NqEwh1kDcpeEnuFC",
"value": 546
},
{
"scriptpubkey": "5121028b45a50f795be0413680036665d17a3eca099648ea80637bc3a70a7d2b52ae2851ae",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_33 028b45a50f795be0413680036665d17a3eca099648ea80637bc3a70a7d2b52ae28 OP_PUSHNUM_1 OP_CHECKMULTISIG",
"scriptpubkey_type": "multisig",
"value": 582
},
{
"scriptpubkey": "a91449ed2c96e33b6134408af8484508bcc3248c8dbd87",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 49ed2c96e33b6134408af8484508bcc3248c8dbd OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "38RuNhSiZiftB6WVnStu5aUz6jXtCDXQZk",
"value": 540
},
{
"scriptpubkey": "0014c8e51cf6891c0a2101aecea8cd5ce9bbbfaf7bba",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 c8e51cf6891c0a2101aecea8cd5ce9bbbfaf7bba",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qerj3ea5frs9zzqdwe65v6h8fhwl677a6s0hxhf",
"value": 294
},
{
"scriptpubkey": "0020c485bbb80c4be276e77eac3a983a391cc8b1a1b5f160995a36c3dff18296385a",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 c485bbb80c4be276e77eac3a983a391cc8b1a1b5f160995a36c3dff18296385a",
"scriptpubkey_type": "v0_p2wsh",
"scriptpubkey_address": "bc1qcjzmhwqvf038dem74safsw3ernytrgd479sfjk3kc00lrq5k8pdqczl83q",
"value": 330
},
{
"scriptpubkey": "5120a7a42b268957a06c9de4d7260f1df392ce4d6e7b743f5adc27415ce2afceb3b9",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 a7a42b268957a06c9de4d7260f1df392ce4d6e7b743f5adc27415ce2afceb3b9",
"scriptpubkey_type": "v1_p2tr",
"scriptpubkey_address": "bc1p57jzkf5f27sxe80y6unq780njt8y6mnmwsl44hp8g9ww9t7wkwusv7av76",
"value": 330
},
{
"scriptpubkey": "51024e73",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_2 4e73",
"scriptpubkey_type": "unknown",
"scriptpubkey_address": "bc1pfeessrawgf",
"value": 240
},
{
"scriptpubkey": "6a224e6f7420796f757220696e707574732c206e6f7420796f7572206f7574707574732e005152535455565758595a5b5c5d5e5f60",
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_34 4e6f7420796f757220696e707574732c206e6f7420796f7572206f7574707574732e OP_0 OP_PUSHNUM_1 OP_PUSHNUM_2 OP_PUSHNUM_3 OP_PUSHNUM_4 OP_PUSHNUM_5 OP_PUSHNUM_6 OP_PUSHNUM_7 OP_PUSHNUM_8 OP_PUSHNUM_9 OP_PUSHNUM_10 OP_PUSHNUM_11 OP_PUSHNUM_12 OP_PUSHNUM_13 OP_PUSHNUM_14 OP_PUSHNUM_15 OP_PUSHNUM_16",
"scriptpubkey_type": "op_return",
"value": 0
}
],
"size": 3500,
"weight": 8186,
"sigops": 115,
"fee": 71294,
"status": {
"confirmed": true,
"block_height": 850000,
"block_hash": "00000000000000000002a0b5db2a7f8d9087464c2586b546be7bce8eb53b8187",
"block_time": 1719689674
}
}
]

View File

@@ -23,7 +23,7 @@ describe('Mempool Backend Config', () => {
UNIX_SOCKET_PATH: '',
SPAWN_CLUSTER_PROCS: 0,
API_URL_PREFIX: '/api/v1/',
AUTOMATIC_BLOCK_REINDEXING: false,
AUTOMATIC_POOLS_UPDATE: false,
POLL_RATE_MS: 2000,
CACHE_DIR: './cache',
CACHE_ENABLED: true,
@@ -41,8 +41,9 @@ describe('Mempool Backend Config', () => {
STDOUT_LOG_MIN_PRIORITY: 'debug',
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
POOLS_UPDATE_DELAY: 604800,
AUDIT: false,
RUST_GBT: false,
RUST_GBT: true,
LIMIT_GBT: false,
CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0,
@@ -63,6 +64,7 @@ describe('Mempool Backend Config', () => {
REQUEST_TIMEOUT: 10000,
FALLBACK_TIMEOUT: 5000,
FALLBACK: [],
MAX_BEHIND_TIP: 2,
});
expect(config.CORE_RPC).toStrictEqual({
@@ -72,7 +74,8 @@ describe('Mempool Backend Config', () => {
PASSWORD: 'mempool',
TIMEOUT: 60000,
COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie'
COOKIE_PATH: '/bitcoin/.cookie',
DEBUG_LOG_PATH: '',
});
expect(config.SECOND_CORE_RPC).toStrictEqual({
@@ -156,6 +159,11 @@ describe('Mempool Backend Config', () => {
PAID: false,
API_KEY: '',
});
expect(config.STRATUM).toStrictEqual({
ENABLED: false,
API: 'http://localhost:1234',
});
});
});

View File

@@ -13,7 +13,7 @@ const vectorBuffer: Buffer = fs.readFileSync(path.join(__dirname, './', './test-
describe('Rust GBT', () => {
test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => {
const rustGbt = new GbtGenerator();
const rustGbt = new GbtGenerator(4_000_000, 8);
const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer);
const result = await rustGbt.make(mempool, [], maxUid);

View File

@@ -70,7 +70,7 @@ class AboutRoutes {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => {
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });

View File

@@ -14,6 +14,7 @@ class AccelerationRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this))
.post(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/estimate', this.$getAcceleratorEstimate.bind(this))
;
}
@@ -64,6 +65,20 @@ class AccelerationRoutes {
res.status(500).end();
}
}
private async $getAcceleratorEstimate(req: Request, res: Response): Promise<void> {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.post(url, req.body, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get acceleration estimate from ${url} in $getAcceleratorEstimate(), ${e}`, this.tag);
res.status(500).end();
}
}
}
export default new AccelerationRoutes();

View File

@@ -1,15 +1,14 @@
import logger from '../../logger';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import { GraphTx, getSameBlockRelatives, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from '../mini-miner';
const BLOCK_WEIGHT_UNITS = 4_000_000;
const BLOCK_SIGOPS = 80_000;
const MAX_RELATIVE_GRAPH_SIZE = 200;
const BID_BOOST_WINDOW = 40_000;
const BID_BOOST_MIN_OFFSET = 10_000;
const BID_BOOST_MAX_OFFSET = 400_000;
type Acceleration = {
export type Acceleration = {
txid: string;
max_bid: number;
};
@@ -28,31 +27,6 @@ export interface AccelerationInfo {
cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
}
interface GraphTx {
txid: string;
vsize: number;
weight: number;
fees: {
base: number; // in sats
};
depends: string[];
spentby: string[];
}
interface MempoolTx extends GraphTx {
ancestorcount: number;
ancestorsize: number;
fees: { // in sats
base: number;
ancestor: number;
};
ancestors: Map<string, MempoolTx>,
ancestorRate: number;
individualRate: number;
score: number;
}
class AccelerationCosts {
/**
* Takes a list of accelerations and verbose block data
@@ -61,7 +35,7 @@ class AccelerationCosts {
* @param accelerationsx
* @param verboseBlock
*/
public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number {
public calculateBoostRate(accelerations: Acceleration[], blockTxs: MempoolTransactionExtended[]): number {
// Run GBT ourselves to calculate accurate effective fee rates
// the list of transactions comes from a mined block, so we already know everything fits within consensus limits
const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);
@@ -170,108 +144,28 @@ class AccelerationCosts {
/**
* Takes an accelerated mined txid and a target rate
* Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
*
* @param txid
* @param medianFeeRate
*
* @param txid
* @param medianFeeRate
*/
public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
// Get same-block transaction ancestors
const allRelatives = this.getSameBlockRelatives(tx, transactions);
const relativesMap = this.initializeRelatives(allRelatives);
const rootTx = relativesMap.get(tx.txid) as MempoolTx;
const allRelatives = getSameBlockRelatives(tx, transactions);
const relativesMap = initializeRelatives(allRelatives);
const rootTx = relativesMap.get(tx.txid) as GraphTx;
// Calculate cost to boost
return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
}
/**
* Takes a raw transaction, and builds a graph of same-block relatives,
* and returns as a MempoolTx
*
* @param tx
*/
private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
for (const tx of transactions) {
blockTxs.set(tx.txid, tx);
for (const vin of tx.vin) {
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
}
}
const relatives: Map<string, GraphTx> = new Map();
const stack: string[] = [tx.txid];
// build set of same-block ancestors
while (stack.length > 0) {
const nextTxid = stack.pop();
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
if (!nextTx || relatives.has(nextTx.txid)) {
continue;
}
const mempoolTx = this.convertToGraphTx(nextTx);
mempoolTx.fees.base = nextTx.fee || 0;
mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[];
mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[];
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
if (txid) {
stack.push(txid);
}
}
relatives.set(mempoolTx.txid, mempoolTx);
}
return relatives;
}
/**
* Takes a raw transaction and converts it to MempoolTx format
* fee and ancestor data is initialized with dummy/null values
*
* @param tx
*/
private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx {
return {
txid: tx.txid,
vsize: Math.ceil(tx.weight / 4),
weight: tx.weight,
fees: {
base: 0, // dummy
},
depends: [], // dummy
spentby: [], //dummy
};
}
private convertGraphToMempoolTx(tx: GraphTx): MempoolTx {
return {
...tx,
fees: {
base: tx.fees.base,
ancestor: tx.fees.base,
},
ancestorcount: 1,
ancestorsize: Math.ceil(tx.weight / 4),
ancestors: new Map<string, MempoolTx>(),
ancestorRate: 0,
individualRate: 0,
score: 0,
};
}
/**
* Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
* Calculate the minimum set of transactions to fee-bump, their total vsize + fees
*
*
* @param tx
* @param ancestors
*/
private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo {
private calculateAccelerationAncestors(tx: GraphTx, relatives: Map<string, GraphTx>, targetFeeRate: number): AccelerationInfo {
// add root tx to the ancestor map
relatives.set(tx.txid, tx);
@@ -283,12 +177,12 @@ class AccelerationCosts {
});
// Initialize individual & ancestor fee rates
relatives.forEach(entry => this.setAncestorScores(entry));
relatives.forEach(entry => setAncestorScores(entry));
// Sort by descending ancestor score
let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
let sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator);
let includedInCluster: Map<string, MempoolTx> | null = null;
let includedInCluster: Map<string, GraphTx> | null = null;
// While highest score >= targetFeeRate
let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
@@ -297,17 +191,17 @@ class AccelerationCosts {
// Grab the highest scoring entry
const best = sortedRelatives.shift();
if (best) {
const cluster = new Map<string, MempoolTx>(best.ancestors?.entries() || []);
const cluster = new Map<string, GraphTx>(best.ancestors?.entries() || []);
if (best.ancestors.has(tx.txid)) {
includedInCluster = cluster;
}
cluster.set(best.txid, best);
// Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
// and update scores, ancestor totals and dependencies for the survivors
this.removeAncestors(cluster, relatives);
removeAncestors(cluster, relatives);
// re-sort
sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator);
}
}
@@ -345,394 +239,6 @@ class AccelerationCosts {
nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
};
}
/**
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
* for each transaction.
*
* @param tx
* @param all
*/
private setAncestors(tx: MempoolTx, all: Map<string, MempoolTx>, visited: Map<string, Map<string, MempoolTx>>, depth: number = 0): Map<string, MempoolTx> {
// sanity check for infinite recursion / too many ancestors (should never happen)
if (depth >= 100) {
logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`);
throw new Error('invalid_tx_dependencies');
}
// initialize the ancestor map for this tx
tx.ancestors = new Map<string, MempoolTx>();
tx.depends.forEach(parentId => {
const parent = all.get(parentId);
if (parent) {
// add the parent
tx.ancestors?.set(parentId, parent);
// check for a cached copy of this parent's ancestors
let ancestors = visited.get(parent.txid);
if (!ancestors) {
// recursively fetch the parent's ancestors
ancestors = this.setAncestors(parent, all, visited, depth + 1);
}
// and add to this tx's map
ancestors.forEach((ancestor, ancestorId) => {
tx.ancestors?.set(ancestorId, ancestor);
});
}
});
visited.set(tx.txid, tx.ancestors);
return tx.ancestors;
}
/**
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
* by running setAncestors on each leaf, and caching intermediate results.
* then initializes ancestor data for each transaction
*
* @param all
*/
private initializeRelatives(all: Map<string, GraphTx>): Map<string, MempoolTx> {
const mempoolTxs = new Map<string, MempoolTx>();
all.forEach(entry => {
mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry));
});
const visited: Map<string, Map<string, MempoolTx>> = new Map();
const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
for (const leaf of leaves) {
this.setAncestors(leaf, mempoolTxs, visited);
}
mempoolTxs.forEach(entry => {
entry.ancestors?.forEach(ancestor => {
entry.ancestorcount++;
entry.ancestorsize += ancestor.vsize;
entry.fees.ancestor += ancestor.fees.base;
});
this.setAncestorScores(entry);
});
return mempoolTxs;
}
/**
* Remove a cluster of transactions from an in-mempool dependency graph
* and update the survivors' scores and ancestors
*
* @param cluster
* @param ancestors
*/
private removeAncestors(cluster: Map<string, MempoolTx>, all: Map<string, MempoolTx>): void {
// remove
cluster.forEach(tx => {
all.delete(tx.txid);
});
// update survivors
all.forEach(tx => {
cluster.forEach(remove => {
if (tx.ancestors?.has(remove.txid)) {
// remove as dependency
tx.ancestors.delete(remove.txid);
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
// update ancestor sizes and fees
tx.ancestorsize -= remove.vsize;
tx.fees.ancestor -= remove.fees.base;
}
});
// recalculate fee rates
this.setAncestorScores(tx);
});
}
/**
* Take a mempool transaction, and set the fee rates and ancestor score
*
* @param tx
*/
private setAncestorScores(tx: MempoolTx): void {
tx.individualRate = tx.fees.base / tx.vsize;
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
}
// Sort by descending score
private mempoolComparator(a, b): number {
return b.score - a.score;
}
}
export default new AccelerationCosts;
interface TemplateTransaction {
txid: string;
order: number;
weight: number;
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
sigops: number;
fee: number;
feeDelta: number;
ancestors: string[];
cluster: string[];
effectiveFeePerVsize: number;
}
interface MinerTransaction extends TemplateTransaction {
inputs: string[];
feePerVsize: number;
relativesSet: boolean;
ancestorMap: Map<string, MinerTransaction>;
children: Set<MinerTransaction>;
ancestorFee: number;
ancestorVsize: number;
ancestorSigops: number;
score: number;
used: boolean;
modified: boolean;
dependencyRate: number;
}
/*
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/
export function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
const auditPool: Map<string, MinerTransaction> = new Map();
const mempoolArray: MinerTransaction[] = [];
candidates.forEach(tx => {
// initializing everything up front helps V8 optimize property access later
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
const feePerVsize = (tx.fee / adjustedVsize);
auditPool.set(tx.txid, {
txid: tx.txid,
order: txidToOrdering(tx.txid),
fee: tx.fee,
feeDelta: 0,
weight: tx.weight,
adjustedVsize,
feePerVsize: feePerVsize,
effectiveFeePerVsize: feePerVsize,
dependencyRate: feePerVsize,
sigops: tx.sigops || 0,
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
relativesSet: false,
ancestors: [],
cluster: [],
ancestorMap: new Map<string, MinerTransaction>(),
children: new Set<MinerTransaction>(),
ancestorFee: 0,
ancestorVsize: 0,
ancestorSigops: 0,
score: 0,
used: false,
modified: false,
});
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
});
// set accelerated effective fee
for (const acceleration of accelerations) {
const tx = auditPool.get(acceleration.txid);
if (tx) {
tx.feeDelta = acceleration.max_bid;
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
tx.effectiveFeePerVsize = tx.feePerVsize;
tx.dependencyRate = tx.feePerVsize;
}
}
// Build relatives graph & calculate ancestor scores
for (const tx of mempoolArray) {
if (!tx.relativesSet) {
setRelatives(tx, auditPool);
}
}
// Sort by descending ancestor score
mempoolArray.sort(priorityComparator);
// Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score)
const blocks: number[][] = [];
let blockWeight = 0;
let blockSigops = 0;
const transactions: MinerTransaction[] = [];
let modified: MinerTransaction[] = [];
const overflow: MinerTransaction[] = [];
let failures = 0;
while (mempoolArray.length || modified.length) {
// skip invalid transactions
while (mempoolArray[0].used || mempoolArray[0].modified) {
mempoolArray.shift();
}
// Select best next package
let nextTx;
const nextPoolTx = mempoolArray[0];
const nextModifiedTx = modified[0];
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
nextTx = nextPoolTx;
mempoolArray.shift();
} else {
modified.shift();
if (nextModifiedTx) {
nextTx = nextModifiedTx;
}
}
if (nextTx && !nextTx?.used) {
// Check if the package fits into this block
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
const clusterTxids = sortedTxSet.map(tx => tx.txid);
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
const used: MinerTransaction[] = [];
while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop();
if (!ancestor) {
continue;
}
ancestor.used = true;
ancestor.usedBy = nextTx.txid;
// update this tx with effective fee rate & relatives data
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
ancestor.effectiveFeePerVsize = effectiveFeeRate;
}
ancestor.cluster = clusterTxids;
transactions.push(ancestor);
blockWeight += ancestor.weight;
blockSigops += ancestor.sigops;
used.push(ancestor);
}
// remove these as valid package ancestors for any descendants remaining in the mempool
if (used.length) {
used.forEach(tx => {
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
});
}
failures = 0;
} else {
// hold this package in an overflow list while we check for smaller options
overflow.push(nextTx);
failures++;
}
}
// this block is full
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
const queueEmpty = !mempoolArray.length && !modified.length;
if (exceededPackageTries || queueEmpty) {
break;
}
}
for (const tx of transactions) {
tx.ancestors = Object.values(tx.ancestorMap);
}
return transactions;
}
// traverse in-mempool ancestors
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives(
tx: MinerTransaction,
mempool: Map<string, MinerTransaction>,
): void {
for (const parent of tx.inputs) {
const parentTx = mempool.get(parent);
if (parentTx && !tx.ancestorMap?.has(parent)) {
tx.ancestorMap.set(parent, parentTx);
parentTx.children.add(tx);
// visit each node only once
if (!parentTx.relativesSet) {
setRelatives(parentTx, mempool);
}
parentTx.ancestorMap.forEach((ancestor) => {
tx.ancestorMap.set(ancestor.txid, ancestor);
});
}
};
tx.ancestorFee = (tx.fee + tx.feeDelta);
tx.ancestorVsize = tx.adjustedVsize || 0;
tx.ancestorSigops = tx.sigops || 0;
tx.ancestorMap.forEach((ancestor) => {
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
tx.ancestorVsize += ancestor.adjustedVsize;
tx.ancestorSigops += ancestor.sigops;
});
tx.score = tx.ancestorFee / tx.ancestorVsize;
tx.relativesSet = true;
}
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
// avoids recursion to limit call stack depth
function updateDescendants(
rootTx: MinerTransaction,
mempool: Map<string, MinerTransaction>,
modified: MinerTransaction[],
clusterRate: number,
): MinerTransaction[] {
const descendantSet: Set<MinerTransaction> = new Set();
// stack of nodes left to visit
const descendants: MinerTransaction[] = [];
let descendantTx: MinerTransaction | undefined;
rootTx.children.forEach(childTx => {
if (!descendantSet.has(childTx)) {
descendants.push(childTx);
descendantSet.add(childTx);
}
});
while (descendants.length) {
descendantTx = descendants.pop();
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
// remove tx as ancestor
descendantTx.ancestorMap.delete(rootTx.txid);
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
descendantTx.ancestorSigops -= rootTx.sigops;
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
if (!descendantTx.modified) {
descendantTx.modified = true;
modified.push(descendantTx);
}
// add this node's children to the stack
descendantTx.children.forEach(childTx => {
// visit each node only once
if (!descendantSet.has(childTx)) {
descendants.push(childTx);
descendantSet.add(childTx);
}
});
}
}
// return new, resorted modified list
return modified.sort(priorityComparator);
}
// Used to sort an array of MinerTransactions by descending ancestor score
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
if (b.score === a.score) {
// tie-break by txid for stability
return a.order - b.order;
} else {
return b.score - a.score;
}
}
// returns the most significant 4 bytes of the txid as an integer
function txidToOrdering(txid: string): number {
return parseInt(
txid.substring(62, 64) +
txid.substring(60, 62) +
txid.substring(58, 60) +
txid.substring(56, 58),
16
);
}
export default new AccelerationCosts;

View File

@@ -2,24 +2,28 @@ import config from '../config';
import logger from '../logger';
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import rbfCache from './rbf-cache';
import transactionUtils from './transaction-utils';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
: { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
}
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
const unseen: string[] = []; // present in the mined block, not in our mempool
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse
const isDisplaced = {};
const isAccelerated = {};
let displacedWeight = 0;
let matchedWeight = 0;
let projectedWeight = 0;
@@ -32,6 +36,7 @@ class Audit {
inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid);
isAccelerated[tx.txid] = true;
}
}
// coinbase is always expected
@@ -113,11 +118,16 @@ class Audit {
} else {
if (rbfCache.has(tx.txid)) {
rbf.push(tx.txid);
} else if (!isDisplaced[tx.txid]) {
if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) {
unseen.push(tx.txid);
}
} else {
if (mempool[tx.txid]) {
prioritized.push(tx.txid);
if (isDisplaced[tx.txid]) {
added.push(tx.txid);
}
} else {
added.push(tx.txid);
unseen.push(tx.txid);
}
}
overflowWeight += tx.weight;
@@ -125,6 +135,8 @@ class Audit {
totalWeight += tx.weight;
}
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
// transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
let maxOverflowRate = 0;
@@ -165,6 +177,7 @@ class Audit {
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return {
unseen,
censored: Object.keys(isCensored),
added,
prioritized,

View File

@@ -1,4 +1,4 @@
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
@@ -23,11 +23,14 @@ export interface AbstractBitcoinApi {
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
$getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]>;
startHealthChecks(): void;
getHealthStatus(): HealthCheckHost[];

View File

@@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult {
},
['reject-reason']?: string,
}
export interface SubmitPackageResult {
package_msg: string;
"tx-results": { [wtxid: string]: TxResult };
"replaced-transactions"?: string[];
}
export interface TxResult {
txid: string;
"other-wtxid"?: string;
vsize?: number;
fees?: {
base: number;
"effective-feerate"?: number;
"effective-includes"?: string[];
};
error?: string;
}

View File

@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import mempool from '../mempool';
@@ -107,8 +107,14 @@ 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.');
async $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
const verboseBlock: IBitcoinApi.VerboseBlock = await this.bitcoindClient.getBlock(hash, 2);
const transactions: IEsploraApi.Transaction[] = [];
for (const tx of verboseBlock.tx) {
const converted = await this.$convertTransaction(tx, true);
transactions.push(converted);
}
return transactions;
}
$getRawBlock(hash: string): Promise<Buffer> {
@@ -159,13 +165,21 @@ class BitcoinApi implements AbstractBitcoinApi {
const mp = mempool.getMempool();
for (const tx in mp) {
for (const vout of mp[tx].vout) {
if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
if (vout.scriptpubkey_address?.indexOf(prefix) === 0) {
found[vout.scriptpubkey_address] = '';
if (Object.keys(found).length >= 10) {
return Object.keys(found);
}
}
}
for (const vin of mp[tx].vin) {
if (vin.prevout?.scriptpubkey_address?.indexOf(prefix) === 0) {
found[vin.prevout?.scriptpubkey_address] = '';
if (Object.keys(found).length >= 10) {
return Object.keys(found);
}
}
}
}
return Object.keys(found);
}
@@ -182,6 +196,10 @@ class BitcoinApi implements AbstractBitcoinApi {
}
}
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult> {
return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined);
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return {
@@ -232,6 +250,15 @@ class BitcoinApi implements AbstractBitcoinApi {
return outspends;
}
async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> {
const txids = await this.$getTxIdsForBlock(blockhash);
return this.$getRawTransaction(txids[0]);
}
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
throw new Error('Method getAddressTransactionSummary not supported by the Bitcoin RPC API.');
}
$getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
@@ -304,6 +331,7 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return'
};

View File

@@ -1,6 +1,11 @@
import { Application, NextFunction, Request, Response } from 'express';
import logger from '../../logger';
import bitcoinClient from './bitcoin-client';
import config from '../../config';
const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i;
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
/**
* Define a set of routes used by the accelerator server
@@ -9,26 +14,26 @@ import bitcoinClient from './bitcoin-client';
class BitcoinBackendRoutes {
private static tag = 'BitcoinBackendRoutes';
public initRoutes(app: Application) {
public initRoutes(app: Application): void {
app
.get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
.get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
.post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
.post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
.get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
.get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
.get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
.get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
;
}
/**
* Disable caching for bitcoin core routes
*
* @param req
* @param res
* @param next
*
* @param req
* @param res
* @param next
*/
private disableCache(req: Request, res: Response, next: NextFunction): void {
res.setHeader('Pragma', 'no-cache');
@@ -39,16 +44,16 @@ class BitcoinBackendRoutes {
/**
* Exeption handler to return proper details to the accelerator server
*
* @param e
* @param fnName
* @param res
*
* @param e
* @param fnName
* @param res
*/
private static handleException(e: any, fnName: string, res: Response): void {
if (typeof(e.code) === 'number') {
res.status(400).send(JSON.stringify(e, ['code', 'message']));
} else {
const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`;
res.status(400).send(JSON.stringify(e, ['code']));
} else {
const err = `unknown exception in ${fnName}`;
logger.err(err, BitcoinBackendRoutes.tag);
res.status(500).send(err);
}
@@ -57,13 +62,13 @@ class BitcoinBackendRoutes {
private async $getMempoolEntry(req: Request, res: Response): Promise<void> {
const txid = req.query.txid;
try {
if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
return;
}
const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
if (!mempoolEntry) {
res.status(404).send(`no mempool entry found for txid ${txid}`);
res.status(404).send();
return;
}
res.status(200).send(mempoolEntry);
@@ -75,13 +80,13 @@ class BitcoinBackendRoutes {
private async $decodeRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx;
try {
if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
return;
}
const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx);
if (!decodedTx) {
res.status(400).send(`unable to decode rawTx ${rawTx}`);
res.status(400).send(`unable to decode rawTx`);
return;
}
res.status(200).send(decodedTx);
@@ -94,23 +99,23 @@ class BitcoinBackendRoutes {
const txid = req.query.txid;
const verbose = req.query.verbose;
try {
if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
return;
}
if (typeof(verbose) !== 'string') {
res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`);
res.status(400).send(`invalid param verbose. must be a string representing an integer`);
return;
}
const verboseNumber = parseInt(verbose, 10);
if (typeof(verboseNumber) !== 'number') {
res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`);
res.status(400).send(`invalid param verbose. must be a valid integer`);
return;
}
const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber);
if (!decodedTx) {
res.status(400).send(`unable to get raw transaction for txid ${txid}`);
res.status(400).send(`unable to get raw transaction`);
return;
}
res.status(200).send(decodedTx);
@@ -122,13 +127,13 @@ class BitcoinBackendRoutes {
private async $sendRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx;
try {
if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
return;
}
const txHex = await bitcoinClient.sendRawTransaction(rawTx);
if (!txHex) {
res.status(400).send(`unable to send rawTx ${rawTx}`);
res.status(400).send(`unable to send rawTx`);
return;
}
res.status(200).send(txHex);
@@ -140,13 +145,13 @@ class BitcoinBackendRoutes {
private async $testMempoolAccept(req: Request, res: Response): Promise<void> {
const rawTxs = req.body.rawTxs;
try {
if (typeof(rawTxs) !== 'object') {
res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`);
if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) {
res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`);
return;
}
const txHex = await bitcoinClient.testMempoolAccept(rawTxs);
if (typeof(txHex) !== 'object' || txHex.length === 0) {
res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`);
res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`);
return;
}
res.status(200).send(txHex);
@@ -159,18 +164,18 @@ class BitcoinBackendRoutes {
const txid = req.query.txid;
const verbose = req.query.verbose;
try {
if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
return;
}
if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) {
res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`);
res.status(400).send(`invalid param verbose. must be a string ('true' | 'false')`);
return;
}
const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false);
if (!ancestors) {
res.status(400).send(`unable to get mempool ancestors for txid ${txid}`);
res.status(400).send(`unable to get mempool ancestors`);
return;
}
res.status(200).send(ancestors);
@@ -183,23 +188,23 @@ class BitcoinBackendRoutes {
const blockHash = req.query.hash;
const verbosity = req.query.verbosity;
try {
if (typeof(blockHash) !== 'string' || blockHash.length !== 64) {
res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`);
if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) {
res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`);
return;
}
if (typeof(verbosity) !== 'string') {
res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`);
res.status(400).send(`invalid param verbosity. must be a string representing an integer`);
return;
}
const verbosityNumber = parseInt(verbosity, 10);
if (typeof(verbosityNumber) !== 'number') {
res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`);
res.status(400).send(`invalid param verbosity. must be a valid integer`);
return;
}
const block = await bitcoinClient.getBlock(blockHash, verbosityNumber);
if (!block) {
res.status(400).send(`unable to get block for block hash ${blockHash}`);
res.status(400).send(`unable to get block`);
return;
}
res.status(200).send(block);
@@ -212,18 +217,18 @@ class BitcoinBackendRoutes {
const blockHeight = req.query.height;
try {
if (typeof(blockHeight) !== 'string') {
res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`);
res.status(400).send(`invalid param blockHeight, must be a string representing an integer`);
return;
}
const blockHeightNumber = parseInt(blockHeight, 10);
if (typeof(blockHeightNumber) !== 'number') {
res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`);
res.status(400).send(`invalid param blockHeight. must be a valid integer`);
return;
}
const block = await bitcoinClient.getBlockHash(blockHeightNumber);
if (!block) {
res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`);
res.status(400).send(`unable to get block hash`);
return;
}
res.status(200).send(block);
@@ -246,4 +251,4 @@ class BitcoinBackendRoutes {
}
}
export default new BitcoinBackendRoutes
export default new BitcoinBackendRoutes;

View File

@@ -19,7 +19,13 @@ import bitcoinClient from './bitcoin-client';
import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
import { calculateCpfp } from '../cpfp';
import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i;
const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i;
class BitcoinRoutes {
public initRoutes(app: Application) {
@@ -41,11 +47,15 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
;
if (config.MEMPOOL.BACKEND !== 'esplora') {
@@ -85,7 +95,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get init data');
}
}
@@ -104,19 +114,22 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks();
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get mempool blocks');
}
}
private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) {
res.status(500).send('Not an array');
handleError(req, res, 500, 'Not an array');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
txIds.push(req.query.txId[_txId].toString());
const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
}
}
@@ -127,12 +140,16 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') {
res.status(500).send('Invalid txids format');
handleError(req, res, 500, 'Invalid txids format');
return;
}
const txids = txids_csv.split(',');
if (txids.length > 50) {
res.status(400).send('Too many txids requested');
handleError(req, res, 400, 'Too many txids requested');
return;
}
if (txids.some((txid) => !TXID_REGEX.test(txid))) {
handleError(req, res, 400, 'Invalid txids format');
return;
}
@@ -140,13 +157,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get batched outspends');
}
}
private async $getCpfpInfo(req: Request, res: Response) {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
res.status(501).send(`Invalid transaction ID.`);
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
@@ -159,14 +176,17 @@ class BitcoinRoutes {
descendants: tx.descendants || null,
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
sigops: tx.sigops,
fee: tx.fee,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined,
feeDelta: tx.feeDelta || undefined,
});
return;
}
const cpfpInfo = calculateCpfp(tx, mempool.getMempool());
const cpfpInfo = calculateMempoolTxCpfp(tx, mempool.getMempool());
res.json(cpfpInfo);
return;
@@ -176,7 +196,7 @@ class BitcoinRoutes {
try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) {
res.status(500).send('failed to get CPFP info');
handleError(req, res, 500, 'Failed to get CPFP info');
return;
}
}
@@ -197,6 +217,10 @@ class BitcoinRoutes {
}
private async getTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
res.json(transaction);
@@ -204,12 +228,18 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
handleError(req, res, statusCode, 'Failed to get transaction');
}
}
private async getRawTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
res.setHeader('content-type', 'text/plain');
@@ -218,8 +248,10 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
handleError(req, res, statusCode, 'Failed to get raw transaction');
}
}
@@ -280,18 +312,22 @@ class BitcoinRoutes {
// Not modified
// 422 Unprocessable Entity
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`);
handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
}
} catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
res.status(404).send(e.message);
handleError(req, res, 404, notFoundError);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to process PSBT');
}
}
}
private async getTransactionStatus(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction.status);
@@ -299,22 +335,54 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
handleError(req, res, statusCode, 'Failed to get transaction status');
}
}
private async getStrippedBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get block summary');
}
}
private async getStrippedBlockTransaction(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
if (!transaction) {
handleError(req, res, 404, `Transaction not found in summary`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transaction);
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction from summary');
}
}
private async getBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const block = await blocks.$getBlock(req.params.hash);
@@ -326,37 +394,69 @@ class BitcoinRoutes {
} else if (blockAge > 30 * day) {
cacheDuration = 10 * day;
} else {
cacheDuration = 600
cacheDuration = 600;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get block');
}
}
private async getBlockHeader(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.setHeader('content-type', 'text/plain');
res.send(blockHeader);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get block header');
}
}
private async getBlockAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
return res.status(404).send(`audit not available`);
handleError(req, res, 404, `Audit not available`);
return;
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get block audit summary');
}
}
private async $getBlockTxAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
handleError(req, res, 404, `Transaction audit not available`);
return;
}
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction audit summary');
}
}
@@ -370,42 +470,49 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get blocks');
}
}
private async getBlocksByBulk(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
return res.status(404).send(`This API is only available for Bitcoin networks`);
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
return;
}
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
return;
}
if (!Common.indexingEnabled()) {
return res.status(404).send(`Indexing is required for this API`);
handleError(req, res, 404, `Indexing is required for this API`);
return;
}
const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) {
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
return;
}
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) {
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
return;
}
if (from > to) {
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
return;
}
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get blocks');
}
}
@@ -440,11 +547,15 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get blocks');
}
}
private async getBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@@ -465,7 +576,7 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get block transactions');
}
}
@@ -474,13 +585,17 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get block at height');
}
}
private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
@@ -489,15 +604,20 @@ class BitcoinRoutes {
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);
handleError(req, res, 413, e.message);
return;
}
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get address');
}
}
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
@@ -510,23 +630,27 @@ class BitcoinRoutes {
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);
handleError(req, res, 413, e.message);
return;
}
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get address transactions');
}
}
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') {
res.status(405).send('Address summary lookups require mempool/electrs backend.');
handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
return;
}
}
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.');
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return;
}
@@ -537,15 +661,20 @@ class BitcoinRoutes {
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);
handleError(req, res, 413, e.message);
return;
}
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get script hash');
}
}
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.');
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return;
}
@@ -560,26 +689,26 @@ class BitcoinRoutes {
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);
handleError(req, res, 413, e.message);
return;
}
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get script hash transactions');
}
}
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') {
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
return;
}
}
private async getAddressPrefix(req: Request, res: Response) {
try {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash);
const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(addressPrefix);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get address prefix');
}
}
@@ -606,7 +735,7 @@ class BitcoinRoutes {
const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -614,12 +743,13 @@ class BitcoinRoutes {
try {
const result = blocks.getCurrentBlockHeight();
if (!result) {
return res.status(503).send(`Service Temporarily Unavailable`);
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'text/plain');
res.send(result.toString());
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get height at tip');
}
}
@@ -629,39 +759,55 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get hash at tip');
}
}
private async getRawBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get raw block');
}
}
private async getTxIdsForBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get txids for block');
}
}
private async validateAddress(req: Request, res: Response) {
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try {
const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to validate address');
}
}
private async getRbfHistory(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const replacements = rbfCache.getRbfTree(req.params.txId) || null;
const replaces = rbfCache.getReplaces(req.params.txId) || null;
@@ -670,7 +816,7 @@ class BitcoinRoutes {
replaces
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get rbf history');
}
}
@@ -679,7 +825,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get rbf trees');
}
}
@@ -688,11 +834,15 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get full rbf replacements');
}
}
private async getCachedTx(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const result = rbfCache.getTx(req.params.txId);
if (result) {
@@ -701,16 +851,20 @@ class BitcoinRoutes {
res.status(204).send();
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get cached tx');
}
}
private async getTransactionOutspends(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get transaction outspends');
}
}
@@ -720,10 +874,10 @@ class BitcoinRoutes {
if (da) {
res.json(da);
} else {
res.status(503).send(`Service Temporarily Unavailable`);
handleError(req, res, 503, `Service Temporarily Unavailable`);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get difficulty change');
}
}
@@ -734,8 +888,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult);
} catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to send raw transaction');
}
}
@@ -746,8 +900,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult);
} catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to send raw transaction');
}
}
@@ -758,9 +912,21 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result);
} catch (e: any) {
res.setHeader('content-type', 'text/plain');
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to test transactions');
}
}
private async $submitPackage(req: Request, res: Response) {
try {
const rawTxs = Common.getTransactionsFromRequest(req);
const maxfeerate = parseFloat(req.query.maxfeerate as string);
const maxburnamount = parseFloat(req.query.maxburnamount as string);
const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to submit package');
}
}

View File

@@ -54,7 +54,7 @@ export namespace IEsploraApi {
scriptpubkey: string;
scriptpubkey_asm: string;
scriptpubkey_type: string;
scriptpubkey_address: string;
scriptpubkey_address?: string;
value: number;
// Elements
valuecommitment?: number;
@@ -179,4 +179,11 @@ export namespace IEsploraApi {
burn_count: number;
}
export interface AddressTxSummary {
txid: string;
value: number;
height: number;
time: number;
tx_position?: number;
}
}

View File

@@ -1,12 +1,12 @@
import config from '../../config';
import axios, { AxiosResponse, isAxiosError } from 'axios';
import axios, { isAxiosError } from 'axios';
import http from 'http';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
import { Common } from '../common';
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import os from 'os';
interface FailoverHost {
host: string,
rtts: number[],
@@ -20,22 +20,37 @@ interface FailoverHost {
preferred?: boolean,
checked: boolean,
lastChecked?: number,
publicDomain: string,
hashes: {
frontend?: string,
backend?: string,
electrs?: string,
lastUpdated: number,
}
}
class FailoverRouter {
activeHost: FailoverHost;
fallbackHost: FailoverHost;
maxSlippage: number = config.ESPLORA.MAX_BEHIND_TIP ?? 2;
maxHeight: number = 0;
hosts: FailoverHost[];
multihost: boolean;
pollInterval: number = 60000;
gitHashInterval: number = 600000; // 10 minutes
pollInterval: number = 60000; // 1 minute
pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create();
localHostname: string = 'localhost';
requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true })
});
constructor() {
try {
this.localHostname = os.hostname();
} catch (e) {
logger.warn('Failed to set local hostname, using "localhost"');
}
// setup list of hosts
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return {
@@ -44,6 +59,10 @@ class FailoverRouter {
rtts: [],
rtt: Infinity,
failures: 0,
publicDomain: 'https://' + this.extractPublicDomain(domain),
hashes: {
lastUpdated: 0,
},
};
});
this.activeHost = {
@@ -54,6 +73,10 @@ class FailoverRouter {
socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true,
checked: false,
publicDomain: `http://${this.localHostname}`,
hashes: {
lastUpdated: 0,
},
};
this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost);
@@ -93,18 +116,36 @@ class FailoverRouter {
);
if (result) {
const height = result.data;
this.maxHeight = Math.max(height, this.maxHeight);
host.latestHeight = height;
this.maxHeight = Math.max(height || 0, ...this.hosts.map(h => (!(h.unreachable || h.timedOut || h.outOfSync) ? h.latestHeight || 0 : 0)));
const rtt = result.config['meta'].rtt;
host.rtts.unshift(rtt);
host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
host.latestHeight = height;
if (height == null || isNaN(height) || (this.maxHeight - height > 2)) {
if (height == null || isNaN(height) || (this.maxHeight - height > this.maxSlippage)) {
host.outOfSync = true;
} else {
host.outOfSync = false;
}
host.unreachable = false;
// update esplora git hash using the x-powered-by header from the height check
const poweredBy = result.headers['x-powered-by'];
if (poweredBy) {
const match = poweredBy.match(/([a-fA-F0-9]{5,40})/);
if (match && match[1]?.length) {
host.hashes.electrs = match[1];
}
}
// Check front and backend git hashes less often
if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) {
await Promise.all([
this.$updateFrontendGitHash(host),
this.$updateBackendGitHash(host)
]);
host.hashes.lastUpdated = Date.now();
}
} else {
host.outOfSync = true;
host.unreachable = true;
@@ -126,7 +167,6 @@ class FailoverRouter {
host.checked = true;
host.lastChecked = Date.now();
// switch if the current host is out of sync or significantly slower than the next best alternative
const rankOrder = this.sortHosts();
// switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== rankOrder[0] && rankOrder[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (rankOrder[0].rtt * 2) + 50)) {
@@ -184,7 +224,6 @@ class FailoverRouter {
// depose the active host and choose the next best replacement
private electHost(): void {
this.activeHost.outOfSync = true;
this.activeHost.failures = 0;
const rankOrder = this.sortHosts();
this.activeHost = rankOrder[0];
@@ -195,6 +234,7 @@ class FailoverRouter {
host.failures++;
if (host.failures > 5 && this.multihost) {
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
this.activeHost.unreachable = true;
this.electHost();
return this.activeHost;
} else {
@@ -202,6 +242,47 @@ class FailoverRouter {
}
}
// methods for retrieving git hashes by host
private async $updateFrontendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/resources/config.js`;
const response = await this.pollConnection.get<string>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/);
if (match && match[1]?.length) {
host.hashes.frontend = match[1];
}
} catch (e) {
// failed to get frontend build hash - do nothing
}
}
private async $updateBackendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/api/v1/backend-info`;
const response = await this.pollConnection.get<any>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
if (response.data?.gitCommit) {
host.hashes.backend = response.data.gitCommit;
}
} catch (e) {
// failed to get backend build hash - do nothing
}
}
// returns the public mempool domain corresponding to an esplora server url
// (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server)
private extractPublicDomain(url: string): string {
// force the url to start with a valid protocol
const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
// parse as URL and extract the hostname
try {
const parsed = new URL(urlWithProtocol);
return parsed.hostname;
} catch (e) {
// fallback to the original url
return url;
}
}
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
let axiosConfig;
let url;
@@ -305,7 +386,7 @@ class ElectrsApi implements AbstractBitcoinApi {
}
$getAddress(address: string): Promise<IEsploraApi.Address> {
throw new Error('Method getAddress not implemented.');
return this.failoverRouter.$get<IEsploraApi.Address>('/address/' + address);
}
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
@@ -332,6 +413,10 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.');
}
$submitPackage(rawTransactions: string[]): Promise<SubmitPackageResult> {
throw new Error('Method not implemented.');
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
}
@@ -352,6 +437,15 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json');
}
async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> {
const txid = await this.failoverRouter.$get<string>(`/block/${blockhash}/txid/0`);
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);
}
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
return this.failoverRouter.$get<IEsploraApi.AddressTxSummary[]>('/address/' + address + '/txs/summary');
}
public startHealthChecks(): void {
this.failoverRouter.startHealthChecks();
}
@@ -368,6 +462,7 @@ class ElectrsApi implements AbstractBitcoinApi {
unreachable: !!host.unreachable,
checked: !!host.checked,
lastChecked: host.lastChecked || 0,
hashes: host.hashes,
}));
} else {
return [];

View File

@@ -2,7 +2,7 @@ import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
@@ -30,6 +30,11 @@ import redisCache from './redis-cache';
import rbfCache from './rbf-cache';
import { calcBitsDifference } from './difficulty-adjustment';
import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -215,10 +220,10 @@ class Blocks {
};
}
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
return {
id: hash,
transactions: Common.classifyTransactions(transactions),
transactions: Common.classifyTransactions(transactions, height),
};
}
@@ -295,10 +300,12 @@ class Blocks {
extras.virtualSize = block.weight / 4.0;
if (coinbaseTx?.vout.length > 0) {
extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null;
extras.coinbaseAddresses = [...new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[])];
extras.coinbaseSignature = coinbaseTx.vout[0].scriptpubkey_asm ?? null;
extras.coinbaseSignatureAscii = transactionUtils.hex2ascii(coinbaseTx.vin[0].scriptsig) ?? null;
} else {
extras.coinbaseAddress = null;
extras.coinbaseAddresses = null;
extras.coinbaseSignature = null;
extras.coinbaseSignatureAscii = null;
}
@@ -336,7 +343,12 @@ class Blocks {
id: pool.uniqueId,
name: pool.name,
slug: pool.slug,
minerNames: null,
};
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
}
extras.matchRate = null;
@@ -370,8 +382,7 @@ class Blocks {
}
}
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter((address) => address);
const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter(address => address) as string[];
let pools: PoolTag[] = [];
if (config.DATABASE.ENABLED === true) {
@@ -380,26 +391,9 @@ class Blocks {
pools = poolsParser.miningPools;
}
for (let i = 0; i < pools.length; ++i) {
if (addresses.length) {
const poolAddresses: string[] = typeof pools[i].addresses === 'string' ?
JSON.parse(pools[i].addresses) : pools[i].addresses;
for (let y = 0; y < poolAddresses.length; y++) {
if (addresses.indexOf(poolAddresses[y]) !== -1) {
return pools[i];
}
}
}
const regexes: string[] = typeof pools[i].regexes === 'string' ?
JSON.parse(pools[i].regexes) : pools[i].regexes;
for (let y = 0; y < regexes.length; ++y) {
const regex = new RegExp(regexes[y], 'i');
const match = asciiScriptSig.match(regex);
if (match !== null) {
return pools[i];
}
}
const pool = poolsParser.matchBlockMiner(txMinerInfo.vin[0].scriptsig, addresses || [], pools);
if (pool) {
return pool;
}
if (config.DATABASE.ENABLED === true) {
@@ -418,8 +412,16 @@ class Blocks {
}
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
if (indexingBlockAmount <= -1) {
indexingBlockAmount = currentBlockHeight + 1;
}
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
// Get all indexed block hash
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
@@ -452,7 +454,7 @@ class Blocks {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
if (cpfpSummary) {
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
@@ -583,8 +585,11 @@ class Blocks {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesWithVersion(0);
const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesWithVersion(0);
const targetSummaryVersion: number = 1;
const targetTemplateVersion: number = 1;
const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesBelowVersion(targetSummaryVersion);
const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesBelowVersion(targetTemplateVersion);
// nothing to do
if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) {
@@ -617,16 +622,24 @@ class Blocks {
for (let height = currentBlockHeight; height >= 0; height--) {
try {
let txs: TransactionExtended[] | null = null;
let txs: MempoolTransactionExtended[] | null = null;
if (unclassifiedBlocks[height]) {
const blockHash = unclassifiedBlocks[height];
// fetch transactions
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx)) || [];
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendMempoolTransaction(tx)) || [];
// add CPFP
const cpfpSummary = Common.calculateCpfp(height, txs, true);
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 1);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
if (!cpfpRepository.compareClusters(cpfpClusters, cpfpSummary.clusters)) {
// CPFP clusters changed - update the compact_cpfp tables
await CpfpRepository.$deleteClustersAt(height);
await this.$saveCpfp(blockHash, height, cpfpSummary);
}
}
await Common.sleep$(250);
}
if (unclassifiedTemplates[height]) {
@@ -652,9 +665,9 @@ class Blocks {
}
templateTxs.push(tx || templateTx);
}
const cpfpSummary = Common.calculateCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as TransactionExtended[], true);
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx;
@@ -690,6 +703,52 @@ class Blocks {
this.classifyingBlocks = false;
}
/**
* [INDEXING] Index missing coinbase addresses for all blocks
*/
public async $indexCoinbaseAddresses(): Promise<void> {
try {
// Get all indexed block hash
const unindexedBlocks = await blocksRepository.$getBlocksWithoutCoinbaseAddresses();
if (!unindexedBlocks?.length) {
return;
}
logger.info(`Indexing missing coinbase addresses for ${unindexedBlocks.length} blocks`);
// Logging
let count = 0;
let countThisRun = 0;
let timer = Date.now() / 1000;
const startedAt = Date.now() / 1000;
for (const { height, hash } of unindexedBlocks) {
// Logging
const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5) {
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = countThisRun / elapsedSeconds;
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
logger.debug(`Indexing coinbase addresses for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`);
timer = Date.now() / 1000;
countThisRun = 0;
}
const coinbaseTx = await bitcoinApi.$getCoinbaseTx(hash);
const addresses = new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[]);
await blocksRepository.$saveCoinbaseAddresses(hash, [...addresses]);
// Logging
count++;
countThisRun++;
}
logger.notice(`coinbase addresses indexing completed: indexed ${count} blocks`);
} catch (e) {
logger.err(`coinbase addresses indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e;
}
}
/**
* [INDEXING] Index all blocks metadata for the mining dashboard
*/
@@ -860,9 +919,14 @@ class Blocks {
}
}
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
let accelerations = Object.values(mempool.getAccelerations());
if (accelerations?.length > 0) {
const pool = await this.$findBlockMiner(transactionUtils.stripCoinbaseTransaction(transactions[0]));
accelerations = accelerations.filter(a => a.pools.includes(pool.uniqueId));
}
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) {
@@ -883,12 +947,12 @@ class Blocks {
const newBlock = await this.$indexBlock(lastBlock.height - i);
this.blocks.push(newBlock);
this.updateTimerProgress(timer, `reindexed block`);
let cpfpSummary;
let newCpfpSummary;
if (config.MEMPOOL.CPFP_INDEXING) {
cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
this.updateTimerProgress(timer, `reindexed block cpfp`);
}
await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height);
this.updateTimerProgress(timer, `reindexed block summary`);
}
await mining.$indexDifficultyAdjustments();
@@ -937,7 +1001,7 @@ class Blocks {
// start async callbacks
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions));
if (block.height % 2016 === 0) {
if (Common.indexingEnabled()) {
@@ -1119,7 +1183,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = tx.flags || Common.getTransactionFlags(tx);
flags = Common.getTransactionFlags(tx, height);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -1134,11 +1198,11 @@ class Blocks {
};
}),
};
summaryVersion = 1;
summaryVersion = cpfpSummary.version;
} else {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(hash, txs);
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@@ -1160,6 +1224,11 @@ class Blocks {
return summary.transactions;
}
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
const txs = await this.$getStrippedBlockTransactions(hash);
return txs.find(tx => tx.txid === txid) || null;
}
/**
* Get 15 blocks
*
@@ -1259,6 +1328,7 @@ class Blocks {
utxoset_size: block.extras.utxoSetSize ?? null,
coinbase_raw: block.extras.coinbaseRaw ?? null,
coinbase_address: block.extras.coinbaseAddress ?? null,
coinbase_addresses: block.extras.coinbaseAddresses ?? null,
coinbase_signature: block.extras.coinbaseSignature ?? null,
coinbase_signature_ascii: block.extras.coinbaseSignatureAscii ?? null,
pool_slug: block.extras.pool.slug ?? null,
@@ -1273,7 +1343,7 @@ class Blocks {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@@ -1328,6 +1398,14 @@ class Blocks {
}
}
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
} else {
return null;
}
}
public getLastDifficultyAdjustmentTime(): number {
return this.lastDifficultyAdjustmentTime;
}
@@ -1344,11 +1422,11 @@ class Blocks {
return this.currentBlockHeight;
}
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise<CpfpSummary | null> {
let transactions = txs;
if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') {
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
}
if (!transactions) {
const block = await bitcoinClient.getBlock(hash, 2);
@@ -1360,7 +1438,7 @@ class Blocks {
}
if (transactions?.length != null) {
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
const summary = calculateFastBlockCpfp(height, transactions);
await this.$saveCpfp(hash, height, summary);

View File

@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { Request } from 'express';
import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';
@@ -10,7 +10,6 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@@ -80,8 +79,8 @@ export class Common {
return arr;
}
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
// For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
@@ -96,7 +95,7 @@ export class Common {
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
});
if (foundMatches?.length) {
matches[addedTx.txid] = [...new Set(foundMatches)];
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
}
});
} else {
@@ -124,7 +123,7 @@ export class Common {
foundMatches.add(deletedTx);
}
if (foundMatches.size) {
matches[addedTx.txid] = [...foundMatches];
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
}
}
}
@@ -139,17 +138,17 @@ export class Common {
const replaced: Set<MempoolTransactionExtended> = new Set();
for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[i];
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
const key = `${vin.txid}:${vin.vout}`;
const match = spendMap.get(key);
if (match && match.txid !== tx.txid) {
replaced.add(match);
// remove this tx from the spendMap
// prevents the same tx being replaced more than once
for (const replacedVin of match.vin) {
const key = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(key);
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(replacedKey);
}
}
const key = `${vin.txid}:${vin.vout}`;
spendMap.delete(key);
}
if (replaced.size) {
@@ -200,10 +199,13 @@ export class Common {
*
* returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
*
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
* For now, just pull out individual rules into versioned functions where necessary.
*/
static isNonStandard(tx: TransactionExtended): boolean {
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
// version
if (tx.version > TX_MAX_STANDARD_VERSION) {
if (this.isNonStandardVersion(tx, height)) {
return true;
}
@@ -250,6 +252,8 @@ export class Common {
}
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true;
} else if (this.isNonStandardAnchor(tx, height)) {
return true;
}
// TODO: bad-witness-nonstandard
}
@@ -258,9 +262,15 @@ export class Common {
let opreturnCount = 0;
for (const vout of tx.vout) {
// scriptpubkey
if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
// (non-standard output type)
return true;
} else if (vout.scriptpubkey_type === 'unknown') {
// undefined segwit version/length combinations are actually standard in outputs
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951
if (vout.scriptpubkey.startsWith('00') || !this.isWitnessProgram(vout.scriptpubkey)) {
return true;
}
} else if (vout.scriptpubkey_type === 'multisig') {
if (!DEFAULT_PERMIT_BAREMULTISIG) {
// bare-multisig
@@ -286,7 +296,7 @@ export class Common {
dustSize += getVarIntLength(dustSize);
// add value size
dustSize += 8;
if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) {
if (Common.isWitnessProgram(vout.scriptpubkey)) {
dustSize += 67;
} else {
dustSize += 148;
@@ -308,6 +318,70 @@ export class Common {
return false;
}
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
static isWitnessProgram(scriptpubkey: string): false | { version: number, program: string } {
if (scriptpubkey.length < 8 || scriptpubkey.length > 84) {
return false;
}
const version = parseInt(scriptpubkey.slice(0,2), 16);
if (version !== 0 && version < 0x51 || version > 0x60) {
return false;
}
const push = parseInt(scriptpubkey.slice(2,4), 16);
if (push + 2 === (scriptpubkey.length / 2)) {
return {
version: version ? version - 0x50 : 0,
program: scriptpubkey.slice(4),
};
}
return false;
}
// Individual versioned standardness rules
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2;
}
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
return false;
}
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
if (
height != null
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true;
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;
@@ -388,16 +462,19 @@ export class Common {
return flags;
}
static getTransactionFlags(tx: TransactionExtended): number {
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
flags &= ~TransactionFlags.cpfp_child;
if (tx.ancestors?.length) {
flags |= TransactionFlags.cpfp_child;
}
flags &= ~TransactionFlags.cpfp_parent;
if (tx.descendants?.length) {
flags |= TransactionFlags.cpfp_parent;
}
flags &= ~TransactionFlags.replacement;
if (tx.replacement) {
flags |= TransactionFlags.replacement;
}
@@ -433,11 +510,10 @@ export class Common {
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
case 'v1_p2tr': {
if (!vin.witness?.length) {
throw new Error('Taproot input missing witness data');
}
flags |= TransactionFlags.p2tr;
flags = Common.isInscription(vin, flags);
if (vin.witness?.length) {
flags = Common.isInscription(vin, flags);
}
} break;
}
} else {
@@ -519,7 +595,7 @@ export class Common {
if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey;
}
// fast but bad heuristic to detect possible coinjoins
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
@@ -535,17 +611,17 @@ export class Common {
flags |= TransactionFlags.batch_payout;
}
if (this.isNonStandard(tx)) {
if (this.isNonStandard(tx, height)) {
flags |= TransactionFlags.nonstandard;
}
return Number(flags);
}
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
let flags = 0;
try {
flags = Common.getTransactionFlags(tx);
flags = Common.getTransactionFlags(tx, height);
} catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -556,8 +632,8 @@ export class Common {
};
}
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
return txs.map(Common.classifyTransaction);
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
return txs.map(tx => Common.classifyTransaction(tx, height));
}
static stripTransaction(tx: TransactionExtended): TransactionStripped {
@@ -780,96 +856,6 @@ export class Common {
}
}
static calculateCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
const txMap: { [txid: string]: TransactionExtended } = {};
// initialize the txMap
for (const tx of transactions) {
txMap[tx.txid] = tx;
}
// reverse pass to identify CPFP clusters
for (let i = transactions.length - 1; i >= 0; i--) {
const tx = transactions[i];
if (!ancestors[tx.txid]) {
let totalFee = 0;
let totalVSize = 0;
clusterTxs.forEach(tx => {
totalFee += tx?.fee || 0;
totalVSize += (tx.weight / 4);
});
const effectiveFeePerVsize = totalFee / totalVSize;
let cluster: CpfpCluster;
if (clusterTxs.length > 1) {
cluster = {
root: clusterTxs[0].txid,
height,
txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
effectiveFeePerVsize,
};
clusters.push(cluster);
}
clusterTxs.forEach(tx => {
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
if (cluster) {
clusterMap[tx.txid] = cluster;
}
});
// reset working vars
clusterTxs = [];
ancestors = {};
}
clusterTxs.push(tx);
tx.vin.forEach(vin => {
ancestors[vin.txid] = true;
});
}
// forward pass to enforce ancestor rate caps
for (const tx of transactions) {
let minAncestorRate = tx.effectiveFeePerVsize;
for (const vin of tx.vin) {
if (txMap[vin.txid]?.effectiveFeePerVsize) {
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
}
}
// check rounded values to skip cases with almost identical fees
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
tx.effectiveFeePerVsize = minAncestorRate;
if (!clusterMap[tx.txid]) {
// add a single-tx cluster to record the dependent rate
const cluster = {
root: tx.txid,
height,
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
effectiveFeePerVsize: minAncestorRate,
};
clusterMap[tx.txid] = cluster;
clusters.push(cluster);
} else {
// update the existing cluster with the dependent rate
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
}
}
}
if (saveRelatives) {
for (const cluster of clusters) {
cluster.txs.forEach((member, index) => {
txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse();
txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse();
txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize;
});
}
}
return {
transactions,
clusters,
};
}
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats {
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
@@ -877,9 +863,10 @@ export class Common {
let medianFee = 0;
let medianWeight = 0;
// calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions
const leftBound = 1995000;
const rightBound = 2005000;
// calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of transactions
const halfWidth = config.MEMPOOL.BLOCK_WEIGHT_UNITS / 800;
const leftBound = Math.floor((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) - halfWidth);
const rightBound = Math.ceil((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) + halfWidth);
for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) {
const left = weightCount;
const right = weightCount + sortedTxs[i].weight;

View File

@@ -1,29 +1,174 @@
import { CpfpInfo, MempoolTransactionExtended } from '../mempool.interfaces';
import { Ancestor, CpfpCluster, CpfpInfo, CpfpSummary, MempoolTransactionExtended, TransactionExtended } from '../mempool.interfaces';
import { GraphTx, convertToGraphTx, expandRelativesGraph, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from './mini-miner';
import memPool from './mempool';
import { Acceleration } from './acceleration/acceleration';
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
const MAX_GRAPH_SIZE = 50; // the maximum number of in-mempool relatives to consider
const MAX_CLUSTER_ITERATIONS = 100;
interface GraphTx extends MempoolTransactionExtended {
depends: string[];
spentby: string[];
ancestorMap: Map<string, GraphTx>;
fees: {
base: number;
ancestor: number;
export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
const txMap: { [txid: string]: TransactionExtended } = {};
// initialize the txMap
for (const tx of transactions) {
txMap[tx.txid] = tx;
}
// reverse pass to identify CPFP clusters
for (let i = transactions.length - 1; i >= 0; i--) {
const tx = transactions[i];
if (!ancestors[tx.txid]) {
let totalFee = 0;
let totalVSize = 0;
clusterTxs.forEach(tx => {
totalFee += tx?.fee || 0;
totalVSize += (tx.weight / 4);
});
const effectiveFeePerVsize = totalFee / totalVSize;
let cluster: CpfpCluster;
if (clusterTxs.length > 1) {
cluster = {
root: clusterTxs[0].txid,
height,
txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
effectiveFeePerVsize,
};
clusters.push(cluster);
}
clusterTxs.forEach(tx => {
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
if (cluster) {
clusterMap[tx.txid] = cluster;
}
});
// reset working vars
clusterTxs = [];
ancestors = {};
}
clusterTxs.push(tx);
tx.vin.forEach(vin => {
ancestors[vin.txid] = true;
});
}
// forward pass to enforce ancestor rate caps
for (const tx of transactions) {
let minAncestorRate = tx.effectiveFeePerVsize;
for (const vin of tx.vin) {
if (txMap[vin.txid]?.effectiveFeePerVsize) {
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
}
}
// check rounded values to skip cases with almost identical fees
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
tx.effectiveFeePerVsize = minAncestorRate;
if (!clusterMap[tx.txid]) {
// add a single-tx cluster to record the dependent rate
const cluster = {
root: tx.txid,
height,
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
effectiveFeePerVsize: minAncestorRate,
};
clusterMap[tx.txid] = cluster;
clusters.push(cluster);
} else {
// update the existing cluster with the dependent rate
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
}
}
}
if (saveRelatives) {
for (const cluster of clusters) {
cluster.txs.forEach((member, index) => {
txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse();
txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse();
txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize;
});
}
}
return {
transactions,
clusters,
version: 1,
};
}
export function calculateGoodBlockCpfp(height: number, transactions: MempoolTransactionExtended[], accelerations: Acceleration[]): CpfpSummary {
const txMap: { [txid: string]: MempoolTransactionExtended } = {};
for (const tx of transactions) {
txMap[tx.txid] = tx;
}
const template = makeBlockTemplate(transactions, accelerations, 1, Infinity, Infinity);
const clusters = new Map<string, string[]>();
for (const tx of template) {
const cluster = tx.cluster || [];
const root = cluster.length ? cluster[cluster.length - 1] : null;
if (cluster.length > 1 && root && !clusters.has(root)) {
clusters.set(root, cluster);
}
txMap[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
}
const clusterArray: CpfpCluster[] = [];
for (const cluster of clusters.values()) {
for (const txid of cluster) {
const mempoolTx = txMap[txid];
if (mempoolTx) {
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let matched = false;
cluster.forEach(relativeTxid => {
if (relativeTxid === txid) {
matched = true;
} else {
const relative = {
txid: relativeTxid,
fee: txMap[relativeTxid].fee,
weight: (txMap[relativeTxid].adjustedVsize * 4) || txMap[relativeTxid].weight,
};
if (matched) {
descendants.push(relative);
} else {
ancestors.push(relative);
}
}
});
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
Object.assign(mempoolTx, { ancestors, descendants, bestDescendant: null, cpfpChecked: true });
}
}
const root = cluster[cluster.length - 1];
clusterArray.push({
root: root,
height,
txs: cluster.reverse().map(txid => ({
txid,
fee: txMap[txid].fee,
weight: (txMap[txid].adjustedVsize * 4) || txMap[txid].weight,
})),
effectiveFeePerVsize: txMap[root].effectiveFeePerVsize,
});
}
return {
transactions: transactions.map(tx => txMap[tx.txid]),
clusters: clusterArray,
version: 2,
};
ancestorcount: number;
ancestorsize: number;
ancestorRate: number;
individualRate: number;
score: number;
}
/**
* Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for
* that transaction (and all others in the same cluster)
*/
export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
tx.cpfpDirty = false;
return {
@@ -32,30 +177,31 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid:
descendants: tx.descendants || [],
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
sigops: tx.sigops,
fee: tx.fee,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
};
}
const ancestorMap = new Map<string, GraphTx>();
const graphTx = mempoolToGraphTx(tx);
const graphTx = convertToGraphTx(tx, memPool.getSpendMap());
ancestorMap.set(tx.txid, graphTx);
const allRelatives = expandRelativesGraph(mempool, ancestorMap);
const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap());
const relativesMap = initializeRelatives(allRelatives);
const cluster = calculateCpfpCluster(tx.txid, relativesMap);
let totalVsize = 0;
let totalFee = 0;
for (const tx of cluster.values()) {
totalVsize += tx.adjustedVsize;
totalFee += tx.fee;
totalVsize += tx.vsize;
totalFee += tx.fees.base;
}
const effectiveFeePerVsize = totalFee / totalVsize;
for (const tx of cluster.values()) {
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
mempool[tx.txid].ancestors = Array.from(tx.ancestorMap.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestorMap.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].bestDescendant = null;
mempool[tx.txid].cpfpChecked = true;
mempool[tx.txid].cpfpDirty = true;
@@ -70,88 +216,12 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid:
descendants: tx.descendants || [],
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
sigops: tx.sigops,
fee: tx.fee,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
};
}
function mempoolToGraphTx(tx: MempoolTransactionExtended): GraphTx {
return {
...tx,
depends: tx.vin.map(v => v.txid),
spentby: tx.vout.map((v, i) => memPool.getFromSpendMap(tx.txid, i)).map(tx => tx?.txid).filter(txid => txid != null) as string[],
ancestorMap: new Map(),
fees: {
base: tx.fee,
ancestor: tx.fee,
},
ancestorcount: 1,
ancestorsize: tx.adjustedVsize,
ancestorRate: 0,
individualRate: 0,
score: 0,
};
}
/**
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
*/
function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>): Map<string, GraphTx> {
const relatives: Map<string, GraphTx> = new Map();
const stack: GraphTx[] = Array.from(ancestors.values());
while (stack.length > 0) {
if (relatives.size > MAX_GRAPH_SIZE) {
return relatives;
}
const nextTx = stack.pop();
if (!nextTx) {
continue;
}
relatives.set(nextTx.txid, nextTx);
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
if (relatives.has(relativeTxid)) {
// already processed this tx
continue;
}
let mempoolTx = ancestors.get(relativeTxid);
if (!mempoolTx && mempool[relativeTxid]) {
mempoolTx = mempoolToGraphTx(mempool[relativeTxid]);
}
if (mempoolTx) {
stack.push(mempoolTx);
}
}
}
return relatives;
}
/**
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
* by running setAncestors on each leaf, and caching intermediate results.
* then initializes ancestor data for each transaction
*
* @param all
*/
function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
const visited: Map<string, Map<string, GraphTx>> = new Map();
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
for (const leaf of leaves) {
setAncestors(leaf, mempoolTxs, visited);
}
mempoolTxs.forEach(entry => {
entry.ancestorMap?.forEach(ancestor => {
entry.ancestorcount++;
entry.ancestorsize += ancestor.adjustedVsize;
entry.fees.ancestor += ancestor.fees.base;
});
setAncestorScores(entry);
});
return mempoolTxs;
}
/**
* Given a root transaction and a list of in-mempool ancestors,
* Calculate the CPFP cluster
@@ -172,10 +242,10 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator);
// Iterate until we reach a cluster that includes our target tx
let maxIterations = MAX_GRAPH_SIZE;
let maxIterations = MAX_CLUSTER_ITERATIONS;
let best = sortedRelatives.shift();
let bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestorMap.has(tx.txid)) && maxIterations > 0) {
let bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestors.has(tx.txid)) && maxIterations > 0) {
maxIterations--;
if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) {
break;
@@ -190,7 +260,7 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
// Grab the next highest scoring entry
best = sortedRelatives.shift();
if (best) {
bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
bestCluster.set(best?.txid, best);
}
}
@@ -199,88 +269,4 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
bestCluster.set(tx.txid, tx);
return bestCluster;
}
/**
* Remove a cluster of transactions from an in-mempool dependency graph
* and update the survivors' scores and ancestors
*
* @param cluster
* @param ancestors
*/
function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
// remove
cluster.forEach(tx => {
all.delete(tx.txid);
});
// update survivors
all.forEach(tx => {
cluster.forEach(remove => {
if (tx.ancestorMap?.has(remove.txid)) {
// remove as dependency
tx.ancestorMap.delete(remove.txid);
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
// update ancestor sizes and fees
tx.ancestorsize -= remove.adjustedVsize;
tx.fees.ancestor -= remove.fees.base;
}
});
// recalculate fee rates
setAncestorScores(tx);
});
}
/**
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
* for each transaction.
*
* @param tx
* @param all
*/
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
// sanity check for infinite recursion / too many ancestors (should never happen)
if (depth > MAX_GRAPH_SIZE) {
return tx.ancestorMap;
}
// initialize the ancestor map for this tx
tx.ancestorMap = new Map<string, GraphTx>();
tx.depends.forEach(parentId => {
const parent = all.get(parentId);
if (parent) {
// add the parent
tx.ancestorMap?.set(parentId, parent);
// check for a cached copy of this parent's ancestors
let ancestors = visited.get(parent.txid);
if (!ancestors) {
// recursively fetch the parent's ancestors
ancestors = setAncestors(parent, all, visited, depth + 1);
}
// and add to this tx's map
ancestors.forEach((ancestor, ancestorId) => {
tx.ancestorMap?.set(ancestorId, ancestor);
});
}
});
visited.set(tx.txid, tx.ancestorMap);
return tx.ancestorMap;
}
/**
* Take a mempool transaction, and set the fee rates and ancestor score
*
* @param tx
*/
function setAncestorScores(tx: GraphTx): GraphTx {
tx.individualRate = (tx.fees.base * 100_000_000) / tx.adjustedVsize;
tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize;
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
return tx;
}
// Sort by descending score
function mempoolComparator(a: GraphTx, b: GraphTx): number {
return b.score - a.score;
}

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 79;
private static currentVersion = 94;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -653,9 +653,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
if (isBitcoin === true) {
await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
}
await this.updateToSchemaVersion(75);
}
@@ -686,6 +688,436 @@ class DatabaseMigration {
`);
await this.updateToSchemaVersion(79);
}
if (databaseSchemaVersion < 80) {
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
await this.updateToSchemaVersion(80);
}
if (databaseSchemaVersion < 81 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81);
}
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
await this.$fixBadV1AuditBlocks();
await this.updateToSchemaVersion(82);
}
if (databaseSchemaVersion < 83 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
await this.updateToSchemaVersion(83);
}
// add new pools indexes
if (databaseSchemaVersion < 84 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
await this.updateToSchemaVersion(84);
}
// lightning channels indexes
if (databaseSchemaVersion < 85 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
await this.updateToSchemaVersion(85);
}
// lightning nodes indexes
if (databaseSchemaVersion < 86 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
await this.updateToSchemaVersion(86);
}
// lightning node sockets indexes
if (databaseSchemaVersion < 87 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
}
// lightning stats indexes
if (databaseSchemaVersion < 88 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
await this.updateToSchemaVersion(88);
}
// geo names indexes
if (databaseSchemaVersion < 89 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
await this.updateToSchemaVersion(89);
}
// hashrates indexes
if (databaseSchemaVersion < 90 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(90);
}
// block audits indexes
if (databaseSchemaVersion < 91 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
await this.updateToSchemaVersion(91);
}
// elements_pegs indexes
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
await this.updateToSchemaVersion(92);
}
// federation_txos indexes
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
await this.updateToSchemaVersion(93);
}
// Unify database schema for all mempool netwoks
// versions above 94 should not use network-specific flags
if (databaseSchemaVersion < 94) {
if (!isBitcoin) {
// Apply all the bitcoin specific migrations to non-bitcoin networks: liquid, liquidtestnet and testnet4 (!)
// Version 5
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
// Version 6
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
// Version 7
await this.$executeQuery('DROP table IF EXISTS hashrates;');
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
// Version 8
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
// Version 9
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
// Version 10
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
// Version 11
await this.$executeQuery(`ALTER TABLE blocks
ADD avg_fee INT UNSIGNED NULL,
ADD avg_fee_rate INT UNSIGNED NULL
`);
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
// Version 12
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 13
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 14
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 17
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
// Version 18
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
// Version 20
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
// Version 22
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
// Version 24
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
// Version 25
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
// Version 26
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
// Version 27
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
// Version 28
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
// Version 29
await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names'));
await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
// Version 30
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
// Version 31
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
// Version 32
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
// Version 33
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
// Version 34
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
// Version 35
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
// Version 36
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
// Version 37
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
// Version 38
await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
await this.updateToSchemaVersion(38);
// Version 39
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
// Version 40
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
// Version 41
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
// Version 42
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
// Version 43
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
// Version 44
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
// Version 45
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
// Version 48
await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
// Version 57
await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`);
// Version 60
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
// Version 61
if (! await this.$checkIfTableExists('blocks_templates')) {
await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
}
await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
// Version 62
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
// Version 63
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
// Version 64
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
// Version 65
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
// Version 67
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)');
// Version 76
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
// Version 81
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
// Version 83
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
// Version 84
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
// Version 85
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
// Version 86
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
// Version 87
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
// Version 88
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
// Version 89
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
// Version 90
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
// Version 91
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
}
if (config.MEMPOOL.NETWORK !== 'liquid') {
// Apply all the liquid specific migrations to all other networks
// Version 68
await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
// Version 71
await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0');
// Version 92
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
// Version 93
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
}
if (config.MEMPOOL.NETWORK !== 'mainnet') {
// Apply all the mainnet specific migrations to all other networks
// Version 69
await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
// Version 70
await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
// Version 77
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
}
await this.updateToSchemaVersion(94);
}
}
/**
@@ -1300,6 +1732,28 @@ class DatabaseMigration {
logger.warn(`Failed to migrate cpfp transaction data`);
}
}
private async $fixBadV1AuditBlocks(): Promise<void> {
const badBlocks = [
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
];
for (const hash of badBlocks) {
try {
await this.$executeQuery(`
UPDATE blocks_audits
SET prioritized_txs = '[]'
WHERE hash = '${hash}'
`, true);
} catch (e) {
continue;
}
}
}
}
export default new DatabaseMigration();

View File

@@ -257,6 +257,7 @@ class DiskCache {
trees: rbfData.rbf.trees,
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
});
}
} catch (e) {

View File

@@ -1,6 +1,9 @@
import config from '../../config';
import { Application, Request, Response } from 'express';
import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
class ChannelsRoutes {
constructor() { }
@@ -22,7 +25,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to search channels by id');
}
}
@@ -38,7 +41,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get channel');
}
}
@@ -53,11 +56,11 @@ class ChannelsRoutes {
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) {
res.status(400).send('Invalid index');
handleError(req, res, 400, 'Invalid index');
return;
}
if (['open', 'active', 'closed'].includes(status) === false) {
res.status(400).send('Invalid status');
handleError(req, res, 400, 'Invalid status');
return;
}
@@ -69,20 +72,23 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString());
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get channels for node');
}
}
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try {
if (!Array.isArray(req.query.txId)) {
res.status(400).send('Not an array');
handleError(req, res, 400, 'Not an array');
return;
}
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
txIds.push(req.query.txId[_txId].toString());
const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
}
}
const channels = await channelsApi.$getChannelsByTransactionId(txIds);
@@ -107,7 +113,7 @@ class ChannelsRoutes {
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get channels by transaction ids');
}
}
@@ -119,7 +125,7 @@ class ChannelsRoutes {
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);
handleError(req, res, 500, 'Failed to get penalty closed channels');
}
}
@@ -132,7 +138,7 @@ class ChannelsRoutes {
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);
handleError(req, res, 500, 'Failed to get channel geodata');
}
}

View File

@@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import channelsApi from './channels.api';
import statisticsApi from './statistics.api';
import { handleError } from '../../utils/api';
class GeneralLightningRoutes {
constructor() { }
@@ -27,7 +29,7 @@ class GeneralLightningRoutes {
channels: channels,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to search for nodes and channels');
}
}
@@ -41,7 +43,7 @@ class GeneralLightningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get lightning statistics');
}
}
@@ -50,7 +52,7 @@ class GeneralLightningRoutes {
const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get lightning statistics');
}
}
}

View File

@@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces';
import { handleError } from '../../utils/api';
class NodesRoutes {
constructor() { }
@@ -31,7 +32,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to search for node');
}
}
@@ -181,13 +182,13 @@ class NodesRoutes {
}
} catch (e) {}
}
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get node group');
}
}
@@ -195,7 +196,7 @@ class NodesRoutes {
try {
const node = await nodesApi.$getNode(req.params.public_key);
if (!node) {
res.status(404).send('Node not found');
handleError(req, res, 404, 'Node not found');
return;
}
res.header('Pragma', 'public');
@@ -203,7 +204,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get node');
}
}
@@ -215,7 +216,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical node stats');
}
}
@@ -223,7 +224,7 @@ class NodesRoutes {
try {
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
if (!node) {
res.status(404).send('Node not found');
handleError(req, res, 404, 'Node not found');
return;
}
res.header('Pragma', 'public');
@@ -231,7 +232,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get fee histogram');
}
}
@@ -247,7 +248,7 @@ class NodesRoutes {
topByChannels: topChannelsNodes,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get nodes ranking');
}
}
@@ -259,7 +260,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get top nodes by capacity');
}
}
@@ -271,7 +272,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get top nodes by channels');
}
}
@@ -283,7 +284,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get oldest nodes');
}
}
@@ -295,7 +296,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get ISP ranking');
}
}
@@ -307,7 +308,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get world nodes');
}
}
@@ -322,7 +323,7 @@ class NodesRoutes {
);
if (country.length === 0) {
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`);
return;
}
@@ -335,7 +336,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get nodes per country');
}
}
@@ -349,7 +350,7 @@ class NodesRoutes {
);
if (isp.length === 0) {
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`);
return;
}
@@ -362,7 +363,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get nodes per ISP');
}
}
@@ -374,7 +375,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get nodes per country');
}
}
}

View File

@@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
import config from '../../config';
import elementsParser from './elements-parser';
import icons from './icons';
import { handleError } from '../../utils/api';
class LiquidRoutes {
public initRoutes(app: Application) {
@@ -42,7 +43,7 @@ class LiquidRoutes {
res.setHeader('content-length', result.length);
res.send(result);
} else {
res.status(404).send('Asset icon not found');
handleError(req, res, 404, 'Asset icon not found');
}
}
@@ -51,7 +52,7 @@ class LiquidRoutes {
if (result) {
res.json(result);
} else {
res.status(404).send('Asset icons not found');
handleError(req, res, 404, 'Asset icons not found');
}
}
@@ -82,7 +83,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pegs by month');
}
}
@@ -94,7 +95,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get reserves by month');
}
}
@@ -106,7 +107,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pegs');
}
}
@@ -118,7 +119,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get reserves');
}
}
@@ -130,7 +131,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get federation audit status');
}
}
@@ -142,7 +143,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get federation addresses');
}
}
@@ -154,7 +155,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get federation addresses');
}
}
@@ -166,7 +167,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get federation utxos');
}
}
@@ -178,7 +179,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get expired utxos');
}
}
@@ -190,7 +191,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get federation utxos number');
}
}
@@ -202,7 +203,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get emergency spent utxos');
}
}
@@ -214,7 +215,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get emergency spent utxos stats');
}
}
@@ -226,7 +227,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pegs list');
}
}
@@ -238,7 +239,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pegs volume daily');
}
}
@@ -250,7 +251,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pegs count');
}
}

View File

@@ -1,12 +1,13 @@
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
import logger from '../logger';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates } from '../mempool.interfaces';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates, PoolTag } from '../mempool.interfaces';
import { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config';
import { Worker } from 'worker_threads';
import path from 'path';
import mempool from './mempool';
import { Acceleration } from './services/acceleration';
import PoolsRepository from '../repositories/PoolsRepository';
const MAX_UINT32 = Math.pow(2, 32) - 1;
@@ -15,12 +16,14 @@ class MempoolBlocks {
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
private txSelectionWorker: Worker | null = null;
private rustInitialized: boolean = false;
private rustGbtGenerator: GbtGenerator = new GbtGenerator();
private rustGbtGenerator: GbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
private nextUid: number = 1;
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
private txidMap: Map<string, number> = new Map(); // map full txids back to short numerical uids
private pools: { [id: number]: PoolTag } = {};
public getMempoolBlocks(): MempoolBlock[] {
return this.mempoolBlocks.map((block) => {
return {
@@ -42,6 +45,18 @@ class MempoolBlocks {
return this.mempoolBlockDeltas;
}
public async updatePools$(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
this.pools = {};
return;
}
const allPools = await PoolsRepository.$getPools();
this.pools = {};
for (const pool of allPools) {
this.pools[pool.uniqueId] = pool;
}
}
private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] {
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
@@ -215,7 +230,7 @@ class MempoolBlocks {
private resetRustGbt(): void {
this.rustInitialized = false;
this.rustGbtGenerator = new GbtGenerator();
this.rustGbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
}
public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
@@ -247,7 +262,7 @@ class MempoolBlocks {
});
// run the block construction algorithm in a separate thread, and wait for a result
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
try {
const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids(
await rustGbt.make(transactions as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
@@ -338,6 +353,9 @@ class MempoolBlocks {
for (const txid of Object.keys(candidates?.txs ?? mempool)) {
if (txid in mempool) {
mempool[txid].cpfpDirty = false;
mempool[txid].ancestors = [];
mempool[txid].descendants = [];
mempool[txid].bestDescendant = null;
}
}
for (const [txid, rate] of rates) {
@@ -351,7 +369,7 @@ class MempoolBlocks {
const lastBlockIndex = blocks.length - 1;
let hasBlockStack = blocks.length >= 8;
let stackWeight;
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
if (hasBlockStack) {
if (blockWeights && blockWeights[7] !== null) {
stackWeight = blockWeights[7];
@@ -362,28 +380,36 @@ class MempoolBlocks {
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
}
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let ancestor: MempoolTransactionExtended;
for (const cluster of clusters) {
for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid];
if (mempoolTx) {
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
// ugly micro-optimization to avoid allocating new arrays
ancestors.length = 0;
descendants.length = 0;
let matched = false;
cluster.forEach(txid => {
ancestor = mempool[txid];
if (txid === memberTxid) {
matched = true;
} else {
if (!mempool[txid]) {
if (!ancestor) {
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
return;
}
const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: (mempool[txid].adjustedVsize * 4),
fee: ancestor.fee,
weight: (ancestor.adjustedVsize * 4),
};
if (matched) {
descendants.push(relative);
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) {
mempoolTx.lastBoosted = ancestor.firstSeen;
}
} else {
ancestors.push(relative);
}
@@ -392,7 +418,20 @@ class MempoolBlocks {
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
// ugly micro-optimization to avoid allocating new arrays or objects
if (mempoolTx.ancestors) {
mempoolTx.ancestors.length = 0;
} else {
mempoolTx.ancestors = [];
}
if (mempoolTx.descendants) {
mempoolTx.descendants.length = 0;
} else {
mempoolTx.descendants = [];
}
mempoolTx.ancestors.push(...ancestors);
mempoolTx.descendants.push(...descendants);
mempoolTx.cpfpChecked = true;
}
}
}
@@ -402,7 +441,10 @@ class MempoolBlocks {
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended;
const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
let acceleration: Acceleration;
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block = blocks[blockIndex];
let totalSize = 0;
let totalVsize = 0;
let totalWeight = 0;
@@ -418,8 +460,9 @@ class MempoolBlocks {
}
}
for (const txid of block) {
if (txid) {
for (let i = 0; i < block.length; i++) {
const txid = block[i];
if (txid in mempool) {
mempoolTx = mempool[txid];
// save position in projected blocks
mempoolTx.position = {
@@ -427,26 +470,40 @@ class MempoolBlocks {
vsize: totalVsize + (mempoolTx.vsize / 2),
};
const acceleration = accelerations[txid];
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
if (txid in accelerations) {
acceleration = accelerations[txid];
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!(ancestor.txid in mempool)) {
continue;
}
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
}
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
}
delete mempoolTx.acceleration;
}
// online calculation of stack-of-blocks fee stats
@@ -464,7 +521,7 @@ class MempoolBlocks {
}
}
}
return this.dataToMempoolBlocks(
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
block,
transactions,
totalSize,
@@ -472,13 +529,13 @@ class MempoolBlocks {
totalFees,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
);
});
};
if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
this.mempoolBlocks = mempoolBlocks;
this.mempoolBlockDeltas = deltas;
this.updateAccelerationPositions(mempool, accelerations, mempoolBlocks);
}
return mempoolBlocks;
@@ -625,6 +682,124 @@ class MempoolBlocks {
tx.acc ? 1 : 0,
];
}
// estimates and saves positions of accelerations in mining partner mempools
private updateAccelerationPositions(mempoolCache: { [txid: string]: MempoolTransactionExtended }, accelerations: { [txid: string]: Acceleration }, mempoolBlocks: MempoolBlockWithTransactions[]): void {
const accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
// keep track of simulated mempool blocks for each active pool
const pools: {
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
} = {};
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => {
let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4);
}
return {
acceleration: acc,
rate: mempoolCache[acc.txid].effectiveFeePerVsize,
vsize
};
}).sort((a, b) => a.rate - b.rate);
// initialize the pool tracker
for (const { acceleration } of accQueue) {
accelerationPositions[acceleration.txid] = [];
for (const pool of acceleration.pools) {
if (!pools[pool]) {
pools[pool] = {
name: this.pools[pool]?.name || 'unknown',
block: 0,
vsize: 0,
accelerations: [],
complete: false,
};
}
pools[pool].accelerations.push(acceleration.txid);
}
for (const ancestor of mempoolCache[acceleration.txid].ancestors || []) {
accelerationPositions[ancestor.txid] = [];
}
}
for (const pool of Object.keys(pools)) {
// if any pools accepted *every* acceleration, we can just use the GBT result positions directly
if (pools[pool].accelerations.length === Object.keys(accelerations).length) {
pools[pool].complete = true;
}
}
let block = 0;
let index = 0;
let next = accQueue.pop();
// build simulated blocks for each pool by taking the best option from
// either the mempool or the list of accelerations.
while (next && block < mempoolBlocks.length) {
while (next && index < mempoolBlocks[block].transactions.length) {
const nextTx = mempoolBlocks[block].transactions[index];
if (next.rate >= (nextTx.rate || (nextTx.fee / nextTx.vsize))) {
for (const pool of next.acceleration.pools) {
if (pools[pool].vsize + next.vsize <= 999_000) {
pools[pool].vsize += next.vsize;
} else {
pools[pool].block++;
pools[pool].vsize = next.vsize;
}
// insert the acceleration into matching pool's blocks
if (pools[pool].complete && mempoolCache[next.acceleration.txid]?.position !== undefined) {
accelerationPositions[next.acceleration.txid].push({
...mempoolCache[next.acceleration.txid].position as { block: number, vsize: number },
poolId: pool,
pool: pools[pool].name
});
} else {
accelerationPositions[next.acceleration.txid].push({
poolId: pool,
pool: pools[pool].name,
block: pools[pool].block,
vsize: pools[pool].vsize - (next.vsize / 2),
});
}
// and any accelerated ancestors
for (const ancestor of mempoolCache[next.acceleration.txid].ancestors || []) {
if (pools[pool].complete && mempoolCache[ancestor.txid]?.position !== undefined) {
accelerationPositions[ancestor.txid].push({
...mempoolCache[ancestor.txid].position as { block: number, vsize: number },
poolId: pool,
pool: pools[pool].name,
});
} else {
accelerationPositions[ancestor.txid].push({
poolId: pool,
pool: pools[pool].name,
block: pools[pool].block,
vsize: pools[pool].vsize - (next.vsize / 2),
});
}
}
}
next = accQueue.pop();
} else {
// skip accelerated transactions and their CPFP ancestors
if (accelerationPositions[nextTx.txid] == null) {
// insert into all pools' blocks
for (const pool of Object.keys(pools)) {
if (pools[pool].vsize + nextTx.vsize <= 999_000) {
pools[pool].vsize += nextTx.vsize;
} else {
pools[pool].block++;
pools[pool].vsize = nextTx.vsize;
}
}
}
index++;
}
}
block++;
index = 0;
}
mempool.setAccelerationPositions(accelerationPositions);
}
}
export default new MempoolBlocks();

View File

@@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import redisCache from './redis-cache';
import blocks from './blocks';
@@ -19,14 +20,16 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private mempoolCandidates: { [txid: string ]: boolean } = {};
private spendMap = new Map<string, MempoolTransactionExtended>();
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
private txPerSecondArray: number[] = [];
private txPerSecond: number = 0;
@@ -73,12 +76,12 @@ class Mempool {
}
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn;
}
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
candidates?: GbtCandidates) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn;
}
@@ -205,7 +208,7 @@ class Mempool {
return txTimes;
}
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes
@@ -352,7 +355,7 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : [];
const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : [];
if (accelerationDelta.length) {
hasChange = true;
}
@@ -361,12 +364,15 @@ class Mempool {
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
this.recentlyDeleted.unshift(deletedTransactions);
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
}
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
this.updateTimerProgress(timer, 'completed async mempool callback');
}
@@ -394,62 +400,11 @@ class Mempool {
return this.accelerations;
}
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
return [];
}
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
try {
const changed: string[] = [];
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
this.accelerations[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
this.accelerations = newAccelerationMap;
return changed;
return accelerationDelta;
} catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return [];
@@ -514,6 +469,14 @@ class Mempool {
}
}
setAccelerationPositions(positions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] }): void {
this.accelerationPositions = positions;
}
getAccelerationPositions(txid: string): { [pool: number]: { poolId: number, pool: string, block: number, vsize: number } } | undefined {
return this.accelerationPositions[txid];
}
private startTimer() {
const state: any = {
start: Date.now(),
@@ -536,16 +499,7 @@ class Mempool {
}
}
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
// Store replaced transactions
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
}
}
}
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
for (const rbfTransaction in rbfTransactions) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions

View File

@@ -0,0 +1,515 @@
import { Acceleration } from './acceleration/acceleration';
import { MempoolTransactionExtended } from '../mempool.interfaces';
import logger from '../logger';
const BLOCK_WEIGHT_UNITS = 4_000_000;
const BLOCK_SIGOPS = 80_000;
const MAX_RELATIVE_GRAPH_SIZE = 100;
export interface GraphTx {
txid: string;
vsize: number;
weight: number;
depends: string[];
spentby: string[];
ancestorcount: number;
ancestorsize: number;
fees: { // in sats
base: number;
ancestor: number;
};
ancestors: Map<string, GraphTx>,
ancestorRate: number;
individualRate: number;
score: number;
}
interface TemplateTransaction {
txid: string;
order: number;
weight: number;
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
sigops: number;
fee: number;
feeDelta: number;
ancestors: string[];
cluster: string[];
effectiveFeePerVsize: number;
}
interface MinerTransaction extends TemplateTransaction {
inputs: string[];
feePerVsize: number;
relativesSet: boolean;
ancestorMap: Map<string, MinerTransaction>;
children: Set<MinerTransaction>;
ancestorFee: number;
ancestorVsize: number;
ancestorSigops: number;
score: number;
used: boolean;
modified: boolean;
dependencyRate: number;
}
/**
* Takes a raw transaction, and builds a graph of same-block relatives,
* and returns as a GraphTx
*
* @param tx
*/
export function getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
for (const tx of transactions) {
blockTxs.set(tx.txid, tx);
for (const vin of tx.vin) {
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
}
}
const relatives: Map<string, GraphTx> = new Map();
const stack: string[] = [tx.txid];
// build set of same-block ancestors
while (stack.length > 0) {
const nextTxid = stack.pop();
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
if (!nextTx || relatives.has(nextTx.txid)) {
continue;
}
const mempoolTx = convertToGraphTx(nextTx, spendMap);
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
if (txid) {
stack.push(txid);
}
}
relatives.set(mempoolTx.txid, mempoolTx);
}
return relatives;
}
/**
* Takes a raw transaction and converts it to GraphTx format
* fee and ancestor data is initialized with dummy/null values
*
* @param tx
*/
export function convertToGraphTx(tx: MempoolTransactionExtended, spendMap?: Map<string, MempoolTransactionExtended | string>): GraphTx {
return {
txid: tx.txid,
vsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
weight: tx.weight,
fees: {
base: tx.fee || 0,
ancestor: tx.fee || 0,
},
depends: (tx.vin.map(vin => vin.txid).filter(depend => depend) as string[]),
spentby: spendMap ? (tx.vout.map((vout, index) => { const spend = spendMap.get(`${tx.txid}:${index}`); return (spend?.['txid'] || spend); }).filter(spent => spent) as string[]) : [],
ancestorcount: 1,
ancestorsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
ancestors: new Map<string, GraphTx>(),
ancestorRate: 0,
individualRate: 0,
score: 0,
};
}
/**
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
*/
export function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>, spendMap: Map<string, MempoolTransactionExtended>): Map<string, GraphTx> {
const relatives: Map<string, GraphTx> = new Map();
const stack: GraphTx[] = Array.from(ancestors.values());
while (stack.length > 0) {
if (relatives.size > MAX_RELATIVE_GRAPH_SIZE) {
return relatives;
}
const nextTx = stack.pop();
if (!nextTx) {
continue;
}
relatives.set(nextTx.txid, nextTx);
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
if (relatives.has(relativeTxid)) {
// already processed this tx
continue;
}
let ancestorTx = ancestors.get(relativeTxid);
if (!ancestorTx && relativeTxid in mempool) {
const mempoolTx = mempool[relativeTxid];
ancestorTx = convertToGraphTx(mempoolTx, spendMap);
}
if (ancestorTx) {
stack.push(ancestorTx);
}
}
}
return relatives;
}
/**
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
* for each transaction.
*
* @param tx
* @param all
*/
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
// sanity check for infinite recursion / too many ancestors (should never happen)
if (depth > MAX_RELATIVE_GRAPH_SIZE) {
logger.warn('cpfp dependency calculation failed: setAncestors reached depth of 100, unable to proceed');
return tx.ancestors;
}
// initialize the ancestor map for this tx
tx.ancestors = new Map<string, GraphTx>();
tx.depends.forEach(parentId => {
const parent = all.get(parentId);
if (parent) {
// add the parent
tx.ancestors?.set(parentId, parent);
// check for a cached copy of this parent's ancestors
let ancestors = visited.get(parent.txid);
if (!ancestors) {
// recursively fetch the parent's ancestors
ancestors = setAncestors(parent, all, visited, depth + 1);
}
// and add to this tx's map
ancestors.forEach((ancestor, ancestorId) => {
tx.ancestors?.set(ancestorId, ancestor);
});
}
});
visited.set(tx.txid, tx.ancestors);
return tx.ancestors;
}
/**
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
* by running setAncestors on each leaf, and caching intermediate results.
* then initializes ancestor data for each transaction
*
* @param all
*/
export function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
const visited: Map<string, Map<string, GraphTx>> = new Map();
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
for (const leaf of leaves) {
setAncestors(leaf, mempoolTxs, visited);
}
mempoolTxs.forEach(entry => {
entry.ancestors?.forEach(ancestor => {
entry.ancestorcount++;
entry.ancestorsize += ancestor.vsize;
entry.fees.ancestor += ancestor.fees.base;
});
setAncestorScores(entry);
});
return mempoolTxs;
}
/**
* Remove a cluster of transactions from an in-mempool dependency graph
* and update the survivors' scores and ancestors
*
* @param cluster
* @param ancestors
*/
export function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
// remove
cluster.forEach(tx => {
all.delete(tx.txid);
});
// update survivors
all.forEach(tx => {
cluster.forEach(remove => {
if (tx.ancestors?.has(remove.txid)) {
// remove as dependency
tx.ancestors.delete(remove.txid);
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
// update ancestor sizes and fees
tx.ancestorsize -= remove.vsize;
tx.fees.ancestor -= remove.fees.base;
}
});
// recalculate fee rates
setAncestorScores(tx);
});
}
/**
* Take a mempool transaction, and set the fee rates and ancestor score
*
* @param tx
*/
export function setAncestorScores(tx: GraphTx): void {
tx.individualRate = tx.fees.base / tx.vsize;
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
}
// Sort by descending score
export function mempoolComparator(a: GraphTx, b: GraphTx): number {
return b.score - a.score;
}
/*
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/
export function makeBlockTemplate(candidates: MempoolTransactionExtended[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
const auditPool: Map<string, MinerTransaction> = new Map();
const mempoolArray: MinerTransaction[] = [];
candidates.forEach(tx => {
// initializing everything up front helps V8 optimize property access later
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
const feePerVsize = (tx.fee / adjustedVsize);
auditPool.set(tx.txid, {
txid: tx.txid,
order: txidToOrdering(tx.txid),
fee: tx.fee,
feeDelta: 0,
weight: tx.weight,
adjustedVsize,
feePerVsize: feePerVsize,
effectiveFeePerVsize: feePerVsize,
dependencyRate: feePerVsize,
sigops: tx.sigops || 0,
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
relativesSet: false,
ancestors: [],
cluster: [],
ancestorMap: new Map<string, MinerTransaction>(),
children: new Set<MinerTransaction>(),
ancestorFee: 0,
ancestorVsize: 0,
ancestorSigops: 0,
score: 0,
used: false,
modified: false,
});
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
});
// set accelerated effective fee
for (const acceleration of accelerations) {
const tx = auditPool.get(acceleration.txid);
if (tx) {
tx.feeDelta = acceleration.max_bid;
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
tx.effectiveFeePerVsize = tx.feePerVsize;
tx.dependencyRate = tx.feePerVsize;
}
}
// Build relatives graph & calculate ancestor scores
for (const tx of mempoolArray) {
if (!tx.relativesSet) {
setRelatives(tx, auditPool);
}
}
// Sort by descending ancestor score
mempoolArray.sort(priorityComparator);
// Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score)
const blocks: number[][] = [];
let blockWeight = 0;
let blockSigops = 0;
const transactions: MinerTransaction[] = [];
let modified: MinerTransaction[] = [];
const overflow: MinerTransaction[] = [];
let failures = 0;
while (mempoolArray.length || modified.length) {
// skip invalid transactions
while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
mempoolArray.shift();
}
// Select best next package
let nextTx;
const nextPoolTx = mempoolArray[0];
const nextModifiedTx = modified[0];
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
nextTx = nextPoolTx;
mempoolArray.shift();
} else {
modified.shift();
if (nextModifiedTx) {
nextTx = nextModifiedTx;
}
}
if (nextTx && !nextTx?.used) {
// Check if the package fits into this block
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
const clusterTxids = sortedTxSet.map(tx => tx.txid);
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
const used: MinerTransaction[] = [];
while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop();
if (!ancestor) {
continue;
}
ancestor.used = true;
ancestor.usedBy = nextTx.txid;
// update this tx with effective fee rate & relatives data
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
ancestor.effectiveFeePerVsize = effectiveFeeRate;
}
ancestor.cluster = clusterTxids;
transactions.push(ancestor);
blockWeight += ancestor.weight;
blockSigops += ancestor.sigops;
used.push(ancestor);
}
// remove these as valid package ancestors for any descendants remaining in the mempool
if (used.length) {
used.forEach(tx => {
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
});
}
failures = 0;
} else {
// hold this package in an overflow list while we check for smaller options
overflow.push(nextTx);
failures++;
}
}
// this block is full
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
const queueEmpty = !mempoolArray.length && !modified.length;
if (exceededPackageTries || queueEmpty) {
break;
}
}
for (const tx of transactions) {
tx.ancestors = Object.values(tx.ancestorMap);
}
return transactions;
}
// traverse in-mempool ancestors
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives(
tx: MinerTransaction,
mempool: Map<string, MinerTransaction>,
): void {
for (const parent of tx.inputs) {
const parentTx = mempool.get(parent);
if (parentTx && !tx.ancestorMap?.has(parent)) {
tx.ancestorMap.set(parent, parentTx);
parentTx.children.add(tx);
// visit each node only once
if (!parentTx.relativesSet) {
setRelatives(parentTx, mempool);
}
parentTx.ancestorMap.forEach((ancestor) => {
tx.ancestorMap.set(ancestor.txid, ancestor);
});
}
};
tx.ancestorFee = (tx.fee + tx.feeDelta);
tx.ancestorVsize = tx.adjustedVsize || 0;
tx.ancestorSigops = tx.sigops || 0;
tx.ancestorMap.forEach((ancestor) => {
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
tx.ancestorVsize += ancestor.adjustedVsize;
tx.ancestorSigops += ancestor.sigops;
});
tx.score = tx.ancestorFee / tx.ancestorVsize;
tx.relativesSet = true;
}
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
// avoids recursion to limit call stack depth
function updateDescendants(
rootTx: MinerTransaction,
mempool: Map<string, MinerTransaction>,
modified: MinerTransaction[],
clusterRate: number,
): MinerTransaction[] {
const descendantSet: Set<MinerTransaction> = new Set();
// stack of nodes left to visit
const descendants: MinerTransaction[] = [];
let descendantTx: MinerTransaction | undefined;
rootTx.children.forEach(childTx => {
if (!descendantSet.has(childTx)) {
descendants.push(childTx);
descendantSet.add(childTx);
}
});
while (descendants.length) {
descendantTx = descendants.pop();
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
// remove tx as ancestor
descendantTx.ancestorMap.delete(rootTx.txid);
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
descendantTx.ancestorSigops -= rootTx.sigops;
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
if (!descendantTx.modified) {
descendantTx.modified = true;
modified.push(descendantTx);
}
// add this node's children to the stack
descendantTx.children.forEach(childTx => {
// visit each node only once
if (!descendantSet.has(childTx)) {
descendants.push(childTx);
descendantSet.add(childTx);
}
});
}
}
// return new, resorted modified list
return modified.sort(priorityComparator);
}
// Used to sort an array of MinerTransactions by descending ancestor score
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
if (b.score === a.score) {
// tie-break by txid for stability
return a.order - b.order;
} else {
return b.score - a.score;
}
}
// returns the most significant 4 bytes of the txid as an integer
function txidToOrdering(txid: string): number {
return parseInt(
txid.substring(62, 64) +
txid.substring(60, 62) +
txid.substring(58, 60) +
txid.substring(56, 58),
16
);
}

View File

@@ -9,6 +9,8 @@ import bitcoinClient from '../bitcoin/bitcoin-client';
import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes {
public initRoutes(app: Application) {
@@ -41,6 +43,8 @@ class MiningRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations)
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals)
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations', this.$getActiveAccelerations)
.post(config.MEMPOOL.API_URL_PREFIX + 'acceleration/request/:txid', this.$requestAcceleration)
;
}
@@ -50,12 +54,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Prices are not available on testnets.');
handleError(req, res, 400, 'Prices are not available on testnets.');
return;
}
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string;
let response;
if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
@@ -68,7 +72,7 @@ class MiningRoutes {
}
res.status(200).send(response);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical prices');
}
}
@@ -81,9 +85,9 @@ class MiningRoutes {
res.json(stats);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
handleError(req, res, 404, e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pool');
}
}
}
@@ -100,9 +104,9 @@ class MiningRoutes {
res.json(poolBlocks);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
handleError(req, res, 404, e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get blocks for pool');
}
}
}
@@ -126,7 +130,7 @@ class MiningRoutes {
res.json(pools);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pools');
}
}
@@ -140,7 +144,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pools');
}
}
@@ -154,7 +158,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pools historical hashrate');
}
}
@@ -169,9 +173,9 @@ class MiningRoutes {
res.json(hashrates);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
handleError(req, res, 404, e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get pool historical hashrate');
}
}
}
@@ -179,7 +183,7 @@ class MiningRoutes {
private async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0;
try {
currentHashrate = await bitcoinClient.getNetworkHashPs();
currentHashrate = await bitcoinClient.getNetworkHashPs(1008);
currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
@@ -200,7 +204,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical hashrate');
}
}
@@ -214,7 +218,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical block fees');
}
}
@@ -232,7 +236,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical block fees');
}
}
@@ -246,7 +250,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical block rewards');
}
}
@@ -260,7 +264,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical block fee rates');
}
}
@@ -278,7 +282,7 @@ class MiningRoutes {
weights: blockWeights
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical block size and weight');
}
}
@@ -290,7 +294,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical difficulty adjustments');
}
}
@@ -300,7 +304,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response);
} catch (e) {
res.status(500).end();
handleError(req, res, 500, 'Failed to get reward stats');
}
}
@@ -314,7 +318,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get historical blocks health');
}
}
@@ -323,7 +327,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) {
res.status(204).send(`This block has not been audited.`);
handleError(req, res, 204, `This block has not been audited.`);
return;
}
@@ -332,7 +336,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get block audit');
}
}
@@ -355,7 +359,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get height from timestamp');
}
}
@@ -368,7 +372,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get block audit scores');
}
}
@@ -381,7 +385,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null');
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get block audit score');
}
}
@@ -391,12 +395,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.');
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get accelerations by pool');
}
}
@@ -406,13 +410,13 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.');
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get accelerations by height');
}
}
@@ -422,12 +426,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.');
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get recent accelerations');
}
}
@@ -437,12 +441,39 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.');
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get acceleration totals');
}
}
private async $getActiveAccelerations(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
} catch (e) {
handleError(req, res, 500, 'Failed to get active accelerations');
}
}
private async $requestAcceleration(req: Request, res: Response): Promise<void> {
res.setHeader('Pragma', 'no-cache');
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
res.setHeader('expires', -1);
try {
accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send();
} catch (e) {
handleError(req, res, 500, 'Failed to request acceleration');
}
}
}

View File

@@ -136,9 +136,13 @@ class Mining {
poolsStatistics['blockCount'] = blockCount;
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
try {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
} catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);

View File

@@ -5,6 +5,9 @@ import PoolsRepository from '../repositories/PoolsRepository';
import { PoolTag } from '../mempool.interfaces';
import diskCache from './disk-cache';
import mining from './mining/mining';
import transactionUtils from './transaction-utils';
import BlocksRepository from '../repositories/BlocksRepository';
import redisCache from './redis-cache';
class PoolsParser {
miningPools: any[] = [];
@@ -37,15 +40,18 @@ class PoolsParser {
/**
* Populate our db with updated mining pool definition
* @param pools
* @param pools
*/
public async migratePoolsJson(): Promise<void> {
// We also need to wipe the backend cache to make sure we don't serve blocks with
// the wrong mining pool (usually happen with unknown blocks)
diskCache.setIgnoreBlocksCache();
redisCache.setIgnoreBlocksCache();
await this.$insertUnknownPool();
let reindexUnknown = false;
for (const pool of this.miningPools) {
if (!pool.id) {
logger.info(`Mining pool ${pool.name} has no unique 'id' defined. Skipping.`);
@@ -57,22 +63,22 @@ class PoolsParser {
logger.err(`Mining pool ${pool.name} must have at least one of the fields 'addresses' or 'regexes'. Skipping.`);
continue;
}
pool.addresses = pool.addresses || [];
pool.regexes = pool.regexes || [];
if (pool.addresses.length === 0 && pool.regexes.length === 0) {
logger.err(`Mining pool ${pool.name} has no 'addresses' nor 'regexes' defined. Skipping.`);
continue;
}
if (pool.addresses.length === 0) {
logger.warn(`Mining pool ${pool.name} has no 'addresses' defined.`);
}
if (pool.regexes.length === 0) {
logger.warn(`Mining pool ${pool.name} has no 'regexes' defined.`);
}
}
const poolDB = await PoolsRepository.$getPoolByUniqueId(pool.id, false);
if (!poolDB) {
@@ -80,7 +86,7 @@ class PoolsParser {
const slug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase();
logger.debug(`Inserting new mining pool ${pool.name}`);
await PoolsRepository.$insertNewMiningPool(pool, slug);
await this.$deleteUnknownBlocks();
reindexUnknown = true;
} else {
if (poolDB.name !== pool.name) {
// Pool has been renamed
@@ -98,7 +104,45 @@ class PoolsParser {
// Pool addresses changed or coinbase tags changed
logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`);
await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes);
await this.$deleteBlocksForPool(poolDB);
reindexUnknown = true;
await this.$reindexBlocksForPool(poolDB.id);
}
}
}
if (reindexUnknown) {
logger.notice(`Updating addresses and/or coinbase tags for unknown mining pool.`);
let unknownPool;
if (config.DATABASE.ENABLED === true) {
unknownPool = await PoolsRepository.$getUnknownPool();
} else {
unknownPool = this.unknownPool;
}
await this.$reindexBlocksForPool(unknownPool.id);
}
}
public matchBlockMiner(scriptsig: string, addresses: string[], pools: PoolTag[]): PoolTag | undefined {
const asciiScriptSig = transactionUtils.hex2ascii(scriptsig);
for (let i = 0; i < pools.length; ++i) {
if (addresses.length) {
const poolAddresses: string[] = typeof pools[i].addresses === 'string' ?
JSON.parse(pools[i].addresses) : pools[i].addresses;
for (let y = 0; y < poolAddresses.length; y++) {
if (addresses.indexOf(poolAddresses[y]) !== -1) {
return pools[i];
}
}
}
const regexes: string[] = typeof pools[i].regexes === 'string' ?
JSON.parse(pools[i].regexes) : pools[i].regexes;
for (let y = 0; y < regexes.length; ++y) {
const regex = new RegExp(regexes[y], 'i');
const match = asciiScriptSig.match(regex);
if (match !== null) {
return pools[i];
}
}
}
@@ -134,68 +178,47 @@ class PoolsParser {
}
/**
* Delete indexed blocks for an updated mining pool
*
* @param pool
* re-index pool assignment for blocks previously associated with pool
*
* @param pool local id of existing pool to reindex
*/
private async $deleteBlocksForPool(pool: PoolTag): Promise<void> {
// Get oldest blocks mined by the pool and assume pools-v2.json updates only concern most recent years
// Ignore early days of Bitcoin as there were no mining pool yet
const [oldestPoolBlock]: any[] = await DB.query(`
SELECT height
private async $reindexBlocksForPool(poolId: number): Promise<void> {
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
if (config.MEMPOOL.NETWORK === 'testnet') {
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
} else if (config.MEMPOOL.NETWORK === 'signet') {
firstKnownBlockPool = 0;
}
const [blocks]: any[] = await DB.query(`
SELECT height, hash, coinbase_raw, coinbase_addresses
FROM blocks
WHERE pool_id = ?
ORDER BY height
LIMIT 1`,
[pool.id]
);
AND height >= ?
ORDER BY height DESC
`, [poolId, firstKnownBlockPool]);
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
if (config.MEMPOOL.NETWORK === 'testnet') {
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
} else if (config.MEMPOOL.NETWORK === 'signet') {
firstKnownBlockPool = 0;
let pools: PoolTag[] = [];
if (config.DATABASE.ENABLED === true) {
pools = await PoolsRepository.$getPools();
} else {
pools = this.miningPools;
}
const oldestBlockHeight = oldestPoolBlock.length ?? 0 > 0 ? oldestPoolBlock[0].height : firstKnownBlockPool;
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${oldestBlockHeight} for re-indexing`);
await DB.query(`
DELETE FROM blocks
WHERE pool_id = ? AND height >= ${oldestBlockHeight}`,
[unknownPool[0].id]
);
logger.notice(`Deleting blocks from ${pool.name} mining pool for re-indexing`);
await DB.query(`
DELETE FROM blocks
WHERE pool_id = ?`,
[pool.id]
);
let changed = 0;
for (const block of blocks) {
const addresses = JSON.parse(block.coinbase_addresses) || [];
const newPool = this.matchBlockMiner(block.coinbase_raw, addresses, pools);
if (newPool && newPool.id !== poolId) {
changed++;
await BlocksRepository.$savePool(block.hash, newPool.id);
}
}
logger.info(`${changed} blocks assigned to a new pool`, logger.tags.mining);
// Re-index hashrates and difficulty adjustments later
mining.reindexHashrateRequested = true;
mining.reindexDifficultyAdjustmentRequested = true;
}
private async $deleteUnknownBlocks(): Promise<void> {
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
if (config.MEMPOOL.NETWORK === 'testnet') {
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
} else if (config.MEMPOOL.NETWORK === 'signet') {
firstKnownBlockPool = 0;
}
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${firstKnownBlockPool} for re-indexing`);
await DB.query(`
DELETE FROM blocks
WHERE pool_id = ? AND height >= ${firstKnownBlockPool}`,
[unknownPool[0].id]
);
// Re-index hashrates and difficulty adjustments later
mining.reindexHashrateRequested = true;
mining.reindexDifficultyAdjustmentRequested = true;
}
}

View File

@@ -1,10 +1,15 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import pricesUpdater from '../../tasks/price-updater';
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
class PricesRoutes {
public initRoutes(app: Application): void {
app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this));
app
.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this))
;
}
private $getCurrentPrices(req: Request, res: Response): void {
@@ -14,6 +19,23 @@ class PricesRoutes {
res.json(pricesUpdater.getLatestPrices());
}
private async $getAllPrices(req: Request, res: Response): Promise<void> {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
try {
const usdPriceHistory = await PricesRepository.$getPricesTimesAndId();
const responseData = usdPriceHistory.map(p => {
return { time: p.time, USD: p.USD };
});
res.status(200).json(responseData);
} catch (e: any) {
logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`);
res.status(403).send();
}
}
}
export default new PricesRoutes();

View File

@@ -44,6 +44,22 @@ interface CacheEvent {
value?: any,
}
/**
* Singleton for tracking RBF trees
*
* Maintains a set of RBF trees, where each tree represents a sequence of
* consecutive RBF replacements.
*
* Trees are identified by the txid of the root transaction.
*
* To maintain consistency, the following invariants must be upheld:
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
* - Existence: X in treeMap => treeMap(X) in rbfTrees
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
*/
class RbfCache {
private replacedBy: Map<string, string> = new Map();
private replaces: Map<string, string[]> = new Map();
@@ -61,6 +77,10 @@ class RbfCache {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
}
/**
* Low level cache operations
*/
private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
@@ -92,8 +112,18 @@ class RbfCache {
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
}
/**
* Basic data structure operations
* must uphold tree invariants
*/
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
if ( !newTxExtended
|| !replaced?.length
|| this.txs.has(newTxExtended.txid)
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
) {
return;
}
@@ -114,6 +144,10 @@ class RbfCache {
if (!replacedTx.rbf) {
txFullRbf = true;
}
if (this.replacedBy.has(replacedTx.txid)) {
// should never happen
continue;
}
this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid);
@@ -140,18 +174,47 @@ class RbfCache {
}
}
newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid;
const newTree = {
tx: newTx,
time: newTime,
fullRbf: treeFullRbf,
replaces: replacedTrees
};
this.addTree(treeId, newTree);
this.updateTreeMap(treeId, newTree);
this.addTree(newTree.tx.txid, newTree);
this.updateTreeMap(newTree.tx.txid, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
/**
* Read-only public interface
*/
public has(txId: string): boolean {
return this.txs.has(txId);
}
@@ -232,32 +295,6 @@ class RbfCache {
return changes;
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
// is the transaction involved in a full rbf replacement?
public isFullRbf(txid: string): boolean {
const treeId = this.treeMap.get(txid);
@@ -271,6 +308,10 @@ class RbfCache {
return tree?.fullRbf;
}
/**
* Cache maintenance & utility functions
*/
private cleanup(): void {
const now = Date.now();
for (const txid of this.expiring.keys()) {
@@ -299,10 +340,6 @@ class RbfCache {
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.removeTree(tx);
}
this.remove(tx);
}
}
@@ -370,14 +407,21 @@ class RbfCache {
};
}
public async load({ txs, trees, expiring, mempool }): Promise<void> {
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
try {
txs.forEach(txEntry => {
this.txs.set(txEntry.value.txid, txEntry.value);
});
this.staleCount = 0;
for (const deflatedTree of trees) {
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
if (tree) {
this.addTree(tree.tx.txid, tree);
this.updateTreeMap(tree.tx.txid, tree);
if (tree.mined) {
this.evict(tree.tx.txid);
}
}
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
@@ -385,6 +429,31 @@ class RbfCache {
}
});
this.staleCount = 0;
// connect cached trees to current mempool transactions
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
for (const tree of this.rbfTrees.values()) {
const tx = this.getTx(tree.tx.txid);
if (!tx || tree.mined) {
continue;
}
for (const vin of tx.vin) {
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
if (conflict && conflict.txid !== tx.txid) {
if (!conflicts[conflict.txid]) {
conflicts[conflict.txid] = {
replacedBy: conflict,
replaces: new Set(),
};
}
conflicts[conflict.txid].replaces.add(tx);
}
}
}
for (const { replacedBy, replaces } of Object.values(conflicts)) {
this.add([...replaces.values()], replacedBy);
}
await this.checkTrees();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup();
@@ -426,6 +495,12 @@ class RbfCache {
return;
}
// if this tx is already in the cache, return early
if (this.treeMap.has(txid)) {
this.removeTree(deflated.key);
return;
}
// recursively reconstruct child trees
for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
@@ -457,10 +532,6 @@ class RbfCache {
fullRbf: treeInfo.fullRbf,
replaces,
};
this.treeMap.set(txid, root);
if (root === txid) {
this.addTree(root, tree);
}
return tree;
}
@@ -511,6 +582,7 @@ class RbfCache {
processTxs(txs);
}
// evict missing transactions
for (const txid of txids) {
if (!found[txid]) {
this.evict(txid, false);

View File

@@ -27,6 +27,7 @@ class RedisCache {
private rbfCacheQueue: { type: string, txid: string, value: any }[] = [];
private rbfRemoveQueue: { type: string, txid: string }[] = [];
private txFlushLimit: number = 10000;
private ignoreBlocksCache = false;
constructor() {
if (config.REDIS.ENABLED) {
@@ -155,7 +156,7 @@ class RedisCache {
const toAdd = this.cacheQueue.slice(0, this.txFlushLimit);
try {
const msetData = toAdd.map(tx => {
const minified: any = { ...tx };
const minified: any = structuredClone(tx);
delete minified.hex;
for (const vin of minified.vin) {
delete vin.inner_redeemscript_asm;
@@ -341,9 +342,7 @@ class RedisCache {
return;
}
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);
@@ -352,15 +351,21 @@ class RedisCache {
const rbfTrees = await this.$getRbfEntries('tree');
const rbfExpirations = await this.$getRbfEntries('exp');
// Set loaded data
blocks.setBlocks(loadedBlocks || []);
blocks.setBlockSummaries(loadedBlockSummaries || []);
// Load & set block data
if (!this.ignoreBlocksCache) {
const loadedBlocks = await this.$getBlocks();
const loadedBlockSummaries = await this.$getBlockSummaries();
blocks.setBlocks(loadedBlocks || []);
blocks.setBlockSummaries(loadedBlockSummaries || []);
}
// Set other data
await memPool.$setMempool(loadedMempool);
await rbfCache.load({
txs: rbfTxs,
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
expiring: rbfExpirations,
mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
});
}
@@ -411,6 +416,10 @@ class RedisCache {
}
return result;
}
public setIgnoreBlocksCache(): void {
this.ignoreBlocksCache = true;
}
}
export default new RedisCache();

View File

@@ -1,7 +1,12 @@
import { WebSocket } from 'ws';
import config from '../../config';
import logger from '../../logger';
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
import { BlockExtended } from '../../mempool.interfaces';
import axios from 'axios';
import mempool from '../mempool';
import websocketHandler from '../websocket-handler';
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
export interface Acceleration {
txid: string,
@@ -10,6 +15,12 @@ export interface Acceleration {
effectiveFee: number,
feeDelta: number,
pools: number[],
positions?: {
[pool: number]: {
block: number,
vbytes: number,
},
},
};
export interface AccelerationHistory {
@@ -25,25 +36,115 @@ export interface AccelerationHistory {
feeDelta: number,
blockHash: string,
blockHeight: number,
pools: {
pool_unique_id: number,
username: string,
}[],
pools: number[];
};
class AccelerationApi {
public async $fetchAccelerations(): Promise<Acceleration[] | null> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
try {
const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`, { responseType: 'json', timeout: 10000 });
return response.data as Acceleration[];
} catch (e) {
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
return null;
private ws: WebSocket | null = null;
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
private _accelerations: Record<string, Acceleration> = {};
private lastPoll = 0;
private lastPing = Date.now();
private lastPong = Date.now();
private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
public constructor() {}
public getAccelerations(): Record<string, Acceleration> {
return this._accelerations;
}
public countMyAccelerationsWithStatus(filter: MyAccelerationStatus): number {
return Object.values(this.myAccelerations).reduce((count, {status}) => { return count + (status === filter ? 1 : 0); }, 0);
}
public accelerationRequested(txid: string): void {
if (this.onDemandPollingEnabled) {
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
}
}
public accelerationConfirmed(): void {
this.forcePoll = true;
}
private async $fetchAccelerations(): Promise<Acceleration[] | null> {
try {
const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 });
return response?.data || [];
} catch (e) {
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
return null;
}
}
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
if (this.useWebsocket && this.websocketConnected) {
return this._accelerations;
}
if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations();
if (accelerations) {
const latestAccelerations = {};
for (const acc of accelerations) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations;
}
} else {
return [];
return this.$updateAccelerationsOnDemand();
}
return null;
}
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
const shouldUpdate = this.forcePoll
|| this.countMyAccelerationsWithStatus('requested') > 0
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
// update accelerations if necessary
if (shouldUpdate) {
const accelerations = await this.$fetchAccelerations();
this.lastPoll = Date.now();
this.forcePoll = false;
if (accelerations) {
const latestAccelerations: Record<string, Acceleration> = {};
// set relevant accelerations to 'accelerating'
for (const acc of accelerations) {
if (this.myAccelerations[acc.txid]) {
latestAccelerations[acc.txid] = acc;
this.myAccelerations[acc.txid] = { status: 'accelerating', added: Date.now(), acceleration: acc };
}
}
// txs that are no longer accelerating are either confirmed or canceled, so mark for expiry
for (const [txid, { status, acceleration }] of Object.entries(this.myAccelerations)) {
if (status === 'accelerating' && !latestAccelerations[txid]) {
this.myAccelerations[txid] = { status: 'done', added: Date.now(), acceleration };
}
}
}
}
// clear expired accelerations (confirmed / failed / not accepted) after 10 minutes
for (const [txid, { status, added }] of Object.entries(this.myAccelerations)) {
if (['requested', 'done'].includes(status) && added < (Date.now() - (1000 * 60 * 10))) {
delete this.myAccelerations[txid];
}
}
const latestAccelerations = {};
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations;
}
public async $fetchAccelerationHistory(page?: number, status?: string): Promise<AccelerationHistory[] | null> {
@@ -74,6 +175,148 @@ class AccelerationApi {
}
return anyAccelerated;
}
// get a list of accelerations that have changed between two sets of accelerations
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
const changed: string[] = [];
const mempoolCache = mempool.getMempool();
for (const acceleration of Object.values(newAccelerationMap)) {
// skip transactions we don't know about
if (!mempoolCache[acceleration.txid]) {
continue;
}
if (oldAccelerationMap[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(oldAccelerationMap)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
return changed;
}
private handleWebsocketMessage(msg: any): void {
if (msg?.accelerations !== null) {
const latestAccelerations = {};
for (const acc of msg?.accelerations || []) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
websocketHandler.handleAccelerationsChanged(this._accelerations);
}
}
public async connectWebsocket(): Promise<void> {
if (this.startedWebsocketLoop) {
return;
}
while (this.useWebsocket) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(this.websocketPath);
this.lastPing = 0;
this.ws.on('open', () => {
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
this.websocketConnected = true;
this.ws?.send(JSON.stringify({
'watch-accelerations': true
}));
});
this.ws.on('error', (error) => {
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
if (error['errors']) {
errMsg += ' - ' + error['errors'].join(' - ');
}
logger.err(errMsg);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Acceleration websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const msg = (isBinary ? data : data.toString()) as string;
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
}
});
this.ws.on('ping', () => {
logger.debug('received ping from acceleration websocket server');
});
this.ws.on('pong', () => {
logger.debug('received pong from acceleration websocket server');
this.lastPong = Date.now();
});
} else if (this.websocketConnected) {
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
logger.warn('No pong received within 10 seconds, terminating connection');
try {
this.ws?.terminate();
} catch (e) {
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
} finally {
this.ws = null;
this.websocketConnected = false;
this.lastPing = 0;
}
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
logger.debug('sending ping to acceleration websocket server');
if (this.ws?.readyState === WebSocket.OPEN) {
try {
this.ws?.ping();
this.lastPing = Date.now();
} catch (e) {
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
}
}
}
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
export default new AccelerationApi();

View File

@@ -0,0 +1,27 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import WalletApi from './wallets';
import { handleError } from '../../utils/api';
class ServicesRoutes {
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
;
}
private async $getWallet(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString());
const walletId = req.params.walletId;
const wallet = await WalletApi.getWallet(walletId);
res.status(200).send(wallet);
} catch (e) {
handleError(req, res, 500, 'Failed to get wallet');
}
}
}
export default new ServicesRoutes();

View File

@@ -0,0 +1,105 @@
import { WebSocket } from 'ws';
import logger from '../../logger';
import config from '../../config';
import websocketHandler from '../websocket-handler';
export interface StratumJob {
pool: number;
height: number;
coinbase: string;
scriptsig: string;
reward: number;
jobId: string;
extraNonce: string;
extraNonce2Size: number;
prevHash: string;
coinbase1: string;
coinbase2: string;
merkleBranches: string[];
version: string;
bits: string;
time: string;
timestamp: number;
cleanJobs: boolean;
received: number;
}
function isStratumJob(obj: any): obj is StratumJob {
return obj
&& typeof obj === 'object'
&& 'pool' in obj
&& 'prevHash' in obj
&& 'height' in obj
&& 'received' in obj
&& 'version' in obj
&& 'timestamp' in obj
&& 'bits' in obj
&& 'merkleBranches' in obj
&& 'cleanJobs' in obj;
}
class StratumApi {
private ws: WebSocket | null = null;
private runWebsocketLoop: boolean = false;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private jobs: Record<string, StratumJob> = {};
public constructor() {}
public getJobs(): Record<string, StratumJob> {
return this.jobs;
}
private handleWebsocketMessage(msg: any): void {
if (isStratumJob(msg)) {
this.jobs[msg.pool] = msg;
websocketHandler.handleNewStratumJob(this.jobs[msg.pool]);
}
}
public async connectWebsocket(): Promise<void> {
if (!config.STRATUM.ENABLED) {
return;
}
this.runWebsocketLoop = true;
if (this.startedWebsocketLoop) {
return;
}
while (this.runWebsocketLoop) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(`${config.STRATUM.API}`);
this.websocketConnected = true;
this.ws.on('open', () => {
logger.info('Stratum websocket opened');
});
this.ws.on('error', (error) => {
logger.err('Stratum websocket error: ' + error);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Stratum websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e));
}
});
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
export default new StratumApi();

View File

@@ -0,0 +1,153 @@
import config from '../../config';
import logger from '../../logger';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import axios from 'axios';
import { TransactionExtended } from '../../mempool.interfaces';
interface WalletAddress {
address: string;
active: boolean;
stats: {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
};
transactions: IEsploraApi.AddressTxSummary[];
lastSync: number;
}
interface Wallet {
name: string;
addresses: Record<string, WalletAddress>;
lastPoll: number;
}
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
class WalletApi {
private wallets: Record<string, Wallet> = {};
private syncing = false;
constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
return acc;
}, {} as Record<string, Wallet>) : {};
}
public getWallet(wallet: string): Record<string, WalletAddress> {
return this.wallets?.[wallet]?.addresses || {};
}
// resync wallet addresses from the services backend
async $syncWallets(): Promise<void> {
if (!config.WALLETS.ENABLED || this.syncing) {
return;
}
this.syncing = true;
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
try {
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`);
const addresses: Record<string, WalletAddress> = response.data;
const addressList: WalletAddress[] = Object.values(addresses);
// sync all current addresses
for (const address of addressList) {
await this.$syncWalletAddress(wallet, address);
}
// remove old addresses
for (const address of Object.keys(wallet.addresses)) {
if (!addresses[address]) {
delete wallet.addresses[address];
}
}
wallet.lastPoll = Date.now();
logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`);
} catch (e) {
logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
this.syncing = false;
}
// resync address transactions from esplora
async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise<void> {
// fetch full transaction data if the address is new or still active and hasn't been synced in the last hour
const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000);
if (refreshTransactions) {
try {
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
const addressInfo = await bitcoinApi.$getAddress(address.address);
const walletAddress: WalletAddress = {
address: address.address,
active: address.active,
transactions: summary,
stats: addressInfo.chain_stats,
lastSync: Date.now(),
};
wallet.addresses[address.address] = walletAddress;
} catch (e) {
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
// check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, IEsploraApi.Transaction[]> {
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {};
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
walletTransactions[walletKey] = [];
for (const tx of blockTxs) {
const funded: Record<string, number> = {};
const spent: Record<string, number> = {};
const fundedCount: Record<string, number> = {};
const spentCount: Record<string, number> = {};
let anyMatch = false;
for (const vin of tx.vin) {
const address = vin.prevout?.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
spentCount[address] = (spentCount[address] ?? 0) + 1;
}
}
for (const vout of tx.vout) {
const address = vout.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
}
}
for (const address of Object.keys({ ...funded, ...spent })) {
// update address stats
wallet.addresses[address].stats.tx_count++;
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0;
wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0;
wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0;
wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0;
// add tx to summary
const txSummary: IEsploraApi.AddressTxSummary = {
txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: block.height,
time: block.timestamp,
};
wallet.addresses[address].transactions?.push(txSummary);
}
if (anyMatch) {
walletTransactions[walletKey].push(tx);
}
}
}
return walletTransactions;
}
}
export default new WalletApi();

View File

@@ -1,7 +1,7 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import statisticsApi from './statistics-api';
import { handleError } from '../../utils/api';
class StatisticsRoutes {
public initRoutes(app: Application) {
app
@@ -65,7 +65,7 @@ class StatisticsRoutes {
}
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, 'Failed to get statistics');
}
}
}

View File

@@ -103,7 +103,7 @@ class TransactionUtils {
}
const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4);
const transactionExtended: TransactionExtended = Object.assign({
vsize: Math.round(transaction.weight / 4),
vsize: transaction.weight / 4,
feePerVsize: feePerVbytes,
effectiveFeePerVsize: feePerVbytes,
}, transaction);
@@ -121,14 +121,15 @@ class TransactionUtils {
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
order: this.txidToOrdering(transaction.txid),
vsize: Math.round(transaction.weight / 4),
vsize,
adjustedVsize,
sigops,
feePerVsize: feePerVbytes,
adjustedFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: effectiveFeePerVsize,
});
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
@@ -338,6 +339,87 @@ class TransactionUtils {
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript];
}
// calculate the most parsimonious set of prioritizations given a list of block transactions
// (i.e. the most likely prioritizations and deprioritizations)
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
// find the longest increasing subsequence of transactions
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
// should be O(n log n)
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
if (X.length < 2) {
return { prioritized: [], deprioritized: [] };
}
const N = X.length;
const P: number[] = new Array(N);
const M: number[] = new Array(N + 1);
M[0] = -1; // undefined so can be set to any value
let L = 0;
for (let i = 0; i < N; i++) {
// Binary search for the smallest positive l ≤ L
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
let lo = 1;
let hi = L + 1;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
if (X[M[mid]].rate > X[i].rate) {
hi = mid;
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
lo = mid + 1;
}
}
// After searching, lo == hi is 1 greater than the
// length of the longest prefix of X[i]
const newL = lo;
// The predecessor of X[i] is the last index of
// the subsequence of length newL-1
P[i] = M[newL - 1];
M[newL] = i;
if (newL > L) {
// If we found a subsequence longer than any we've
// found yet, update L
L = newL;
}
}
// Reconstruct the longest increasing subsequence
// It consists of the values of X at the L indices:
// ..., P[P[M[L]]], P[M[L]], M[L]
const LIS: any[] = new Array(L);
let k = M[L];
for (let j = L - 1; j >= 0; j--) {
LIS[j] = X[k];
k = P[k];
}
const lisMap = new Map<string, number>();
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
const prioritized: string[] = [];
const deprioritized: string[] = [];
let lastRate = X[0].rate;
for (const tx of X) {
if (lisMap.has(tx.txid)) {
lastRate = tx.rate;
} else {
if (Math.abs(tx.rate - lastRate) < 0.1) {
// skip if the rate is almost the same as the previous transaction
} else if (tx.rate <= lastRate) {
prioritized.push(tx.txid);
} else {
deprioritized.push(tx.txid);
}
}
}
return { prioritized, deprioritized };
}
}
export default new TransactionUtils();

View File

@@ -3,7 +3,7 @@ import * as WebSocket from 'ws';
import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
MempoolDelta, MempoolDeltaTxids
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -16,16 +16,19 @@ import transactionUtils from './transaction-utils';
import rbfCache, { ReplacementInfo } from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api';
import BlocksRepository from '../repositories/BlocksRepository';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import Audit from './audit';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
import statistics from './statistics/statistics';
import accelerationRepository from '../repositories/AccelerationRepository';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import walletApi from './services/wallets';
interface AddressTransactions {
mempool: MempoolTransactionExtended[],
@@ -33,7 +36,9 @@ interface AddressTransactions {
removed: MempoolTransactionExtended[],
}
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateCpfp } from './cpfp';
import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read';
import stratumApi, { StratumJob } from './services/stratum';
// valid 'want' subscriptions
const wantable = [
@@ -57,6 +62,8 @@ class WebsocketHandler {
private lastRbfSummary: ReplacementInfo[] | null = null;
private mempoolSequence: number = 0;
private accelerations: Record<string, Acceleration> = {};
constructor() { }
addWebsocketServer(wss: WebSocket.Server) {
@@ -206,7 +213,8 @@ class WebsocketHandler {
}
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position
position,
accelerationPositions: memPool.getAccelerationPositions(tx.txid),
});
}
} else {
@@ -304,6 +312,14 @@ class WebsocketHandler {
}
}
if (parsedMessage && parsedMessage['track-wallet']) {
if (parsedMessage['track-wallet'] === 'stop') {
client['track-wallet'] = null;
} else {
client['track-wallet'] = parsedMessage['track-wallet'];
}
}
if (parsedMessage && parsedMessage['track-asset']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
client['track-asset'] = parsedMessage['track-asset'];
@@ -388,6 +404,16 @@ class WebsocketHandler {
delete client['track-mempool'];
}
if (parsedMessage && parsedMessage['track-stratum'] != null) {
if (parsedMessage['track-stratum']) {
const sub = parsedMessage['track-stratum'];
client['track-stratum'] = sub;
response['stratumJobs'] = this.socketData['stratumJobs'];
} else {
client['track-stratum'] = false;
}
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
@@ -483,6 +509,42 @@ class WebsocketHandler {
}
}
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server has been set');
}
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
if (!websocketAccelerationDelta.length) {
return;
}
// pre-compute acceleration delta
const accelerationUpdate = {
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
};
try {
const response = JSON.stringify({
accelerations: accelerationUpdate,
});
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
} catch (e) {
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
}
}
handleReorg(): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
@@ -519,8 +581,17 @@ class WebsocketHandler {
}
}
/**
*
* @param newMempool
* @param mempoolSize
* @param newTransactions array of transactions added this mempool update.
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
* @param accelerationDelta
* @param candidates
*/
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
@@ -528,6 +599,8 @@ class WebsocketHandler {
this.printLogs();
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
let added = newTransactions;
let removed = deletedTransactions;
@@ -537,18 +610,18 @@ class WebsocketHandler {
}
if (config.MEMPOOL.RUST_GBT) {
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, config.MEMPOOL_SERVICES.ACCELERATIONS);
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, true);
} else {
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, true);
}
const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations();
const accelerations = accelerationApi.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements;
@@ -577,7 +650,7 @@ class WebsocketHandler {
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid].replaced) {
replacedTransactions.push({ replaced: replaced.txid, by: tx });
}
}
@@ -656,10 +729,13 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
// pre-compute acceleration delta
const accelerationUpdate = {
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]),
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
};
// TODO - Fix indentation after PR is merged
@@ -821,10 +897,13 @@ class WebsocketHandler {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
}
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
};
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
calculateCpfp(mempoolTx, newMempool);
calculateMempoolTxCpfp(mempoolTx, newMempool);
}
if (mempoolTx.cpfpDirty) {
positionData['cpfp'] = {
@@ -834,7 +913,7 @@ class WebsocketHandler {
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
sigops: mempoolTx.sigops,
adjustedVsize: mempoolTx.adjustedVsize,
acceleration: mempoolTx.acceleration
acceleration: mempoolTx.acceleration,
};
}
response['txPosition'] = JSON.stringify(positionData);
@@ -860,9 +939,11 @@ class WebsocketHandler {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
if (!mempoolTx.cpfpChecked) {
calculateCpfp(mempoolTx, newMempool);
calculateMempoolTxCpfp(mempoolTx, newMempool);
}
if (mempoolTx.cpfpDirty) {
txInfo.cpfp = {
@@ -927,6 +1008,8 @@ class WebsocketHandler {
throw new Error('No WebSocket.Server have been set');
}
const blockTransactions = structuredClone(transactions);
this.printLogs();
await statistics.runStatistics();
@@ -936,31 +1019,27 @@ class WebsocketHandler {
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
const accelerations = Object.values(mempool.getAccelerations());
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions);
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.handleRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
let projectedBlocks;
const auditMempool = _memPool;
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
const isAccelerated = accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
if (config.MEMPOOL.RUST_GBT) {
const added = memPool.limitGBT ? (candidates?.added || []) : [];
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
} else {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
}
if (config.MEMPOOL.RUST_GBT) {
const added = memPool.limitGBT ? (candidates?.added || []) : [];
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
} else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
}
if (Common.indexingEnabled()) {
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@@ -982,9 +1061,11 @@ class WebsocketHandler {
});
BlocksAuditsRepository.$saveAudit({
version: 1,
time: block.timestamp,
height: block.height,
hash: block.id,
unseenTxs: unseen,
addedTxs: added,
prioritizedTxs: prioritized,
missingTxs: censored,
@@ -1011,6 +1092,14 @@ class WebsocketHandler {
}
}
if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) {
const firstSeen = getRecentFirstSeen(block.id);
if (firstSeen) {
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
block.extras.firstSeen = firstSeen;
}
}
const confirmedTxids: { [txid: string]: boolean } = {};
// Update mempool to remove transactions included in the new block
@@ -1036,7 +1125,7 @@ class WebsocketHandler {
const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions;
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true);
} else {
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, true);
}
const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
@@ -1085,6 +1174,9 @@ class WebsocketHandler {
replaced: replacedTransactions,
};
// check for wallet transactions
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string {
if (!responseCache[key]) {
@@ -1137,7 +1229,10 @@ class WebsocketHandler {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
}
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
});
}
}
@@ -1157,6 +1252,8 @@ class WebsocketHandler {
},
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
}
}
@@ -1284,6 +1381,11 @@ class WebsocketHandler {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
}
if (client['track-wallet']) {
const trackedWallet = client['track-wallet'];
response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {});
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
@@ -1293,11 +1395,28 @@ class WebsocketHandler {
await statistics.runStatistics();
}
public handleNewStratumJob(job: StratumJob): void {
this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
client.send(JSON.stringify({
'stratumJob': job
}));
}
});
}
}
// takes a dictionary of JSON serialized values
// and zips it together into a valid JSON object
private serializeResponse(response): string {
return '{'
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
+ Object.keys(response).filter(key => response[key] != null).map(key => `"${key}": ${response[key]}`).join(', ')
+ '}';
}

View File

@@ -29,9 +29,10 @@ interface IConfig {
EXTERNAL_RETRY_INTERVAL: number;
USER_AGENT: string;
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
AUTOMATIC_BLOCK_REINDEXING: boolean;
AUTOMATIC_POOLS_UPDATE: boolean;
POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string,
POOLS_UPDATE_DELAY: number,
AUDIT: boolean;
RUST_GBT: boolean;
LIMIT_GBT: boolean;
@@ -51,6 +52,7 @@ interface IConfig {
REQUEST_TIMEOUT: number;
FALLBACK_TIMEOUT: number;
FALLBACK: string[];
MAX_BEHIND_TIP: number;
};
LIGHTNING: {
ENABLED: boolean;
@@ -84,6 +86,7 @@ interface IConfig {
TIMEOUT: number;
COOKIE: boolean;
COOKIE_PATH: string;
DEBUG_LOG_PATH: string;
};
SECOND_CORE_RPC: {
HOST: string;
@@ -159,6 +162,14 @@ interface IConfig {
PAID: boolean;
API_KEY: string;
},
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
},
STRATUM: {
ENABLED: boolean;
API: string;
}
}
const defaults: IConfig = {
@@ -188,11 +199,12 @@ const defaults: IConfig = {
'EXTERNAL_RETRY_INTERVAL': 0,
'USER_AGENT': 'mempool',
'STDOUT_LOG_MIN_PRIORITY': 'debug',
'AUTOMATIC_BLOCK_REINDEXING': false,
'AUTOMATIC_POOLS_UPDATE': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
'AUDIT': false,
'RUST_GBT': false,
'RUST_GBT': true,
'LIMIT_GBT': false,
'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0,
@@ -210,6 +222,7 @@ const defaults: IConfig = {
'REQUEST_TIMEOUT': 10000,
'FALLBACK_TIMEOUT': 5000,
'FALLBACK': [],
'MAX_BEHIND_TIP': 2,
},
'ELECTRUM': {
'HOST': '127.0.0.1',
@@ -223,7 +236,8 @@ const defaults: IConfig = {
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie'
'COOKIE_PATH': '/bitcoin/.cookie',
'DEBUG_LOG_PATH': '',
},
'SECOND_CORE_RPC': {
'HOST': '127.0.0.1',
@@ -318,6 +332,14 @@ const defaults: IConfig = {
'PAID': false,
'API_KEY': '',
},
'WALLETS': {
'ENABLED': false,
'WALLETS': [],
},
'STRATUM': {
'ENABLED': false,
'API': 'http://localhost:1234',
}
};
class Config implements IConfig {
@@ -339,6 +361,8 @@ class Config implements IConfig {
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS'];
STRATUM: IConfig['STRATUM'];
constructor() {
const configs = this.merge(configFromFile, defaults);
@@ -360,6 +384,8 @@ class Config implements IConfig {
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS;
this.STRATUM = configs.STRATUM;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -2,8 +2,7 @@ import * as fs from 'fs';
import path from 'path';
import config from './config';
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
import { LogLevel } from './logger';
import logger from './logger';
import logger, { LogLevel } from './logger';
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
import { execSync } from 'child_process';

View File

@@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes';
import liquidRoutes from './api/liquid/liquid.routes';
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
import servicesRoutes from './api/services/services-routes';
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater';
@@ -45,6 +46,9 @@ import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets';
import stratumApi from './api/services/stratum';
class Server {
private wss: WebSocket.Server | undefined;
@@ -149,6 +153,7 @@ class Server {
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$();
await mempoolBlocks.updatePools$();
if (config.MEMPOOL.ENABLED) {
if (config.MEMPOOL.CACHE_ENABLED) {
await diskCache.$loadMempoolCache();
@@ -209,6 +214,8 @@ class Server {
}
});
}
poolsUpdater.$startService();
}
async runMainUpdateLoop(): Promise<void> {
@@ -227,13 +234,17 @@ class Server {
const newMempool = await bitcoinApi.$getRawMempool();
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
const newAccelerations = await accelerationApi.$fetchAccelerations();
const latestAccelerations = await accelerationApi.$updateAccelerations();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
}
indexer.$run();
if (config.WALLETS.ENABLED) {
// might take a while, so run in the background
walletApi.$syncWallets();
}
if (config.FIAT_PRICE.ENABLED) {
priceUpdater.$run();
}
@@ -308,11 +319,18 @@ class Server {
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
}
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket();
if (config.STRATUM.ENABLED) {
stratumApi.connectWebsocket();
}
}
setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app);
bitcoinCoreRoutes.initRoutes(this.app);
if (config.MEMPOOL.OFFICIAL) {
bitcoinCoreRoutes.initRoutes(this.app);
}
pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app);
@@ -331,7 +349,12 @@ class Server {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app);
}
aboutRoutes.initRoutes(this.app);
if (config.WALLETS.ENABLED) {
servicesRoutes.initRoutes(this.app);
}
if (!config.MEMPOOL.OFFICIAL) {
aboutRoutes.initRoutes(this.app);
}
}
healthCheck(): void {

View File

@@ -10,6 +10,7 @@ import config from './config';
import auditReplicator from './replication/AuditReplication';
import statisticsReplicator from './replication/StatisticsReplication';
import AccelerationRepository from './repositories/AccelerationRepository';
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
export interface CoreIndex {
name: string;
@@ -182,6 +183,7 @@ class Indexer {
}
this.runSingleTask('blocksPrices');
await blocks.$indexCoinbaseAddresses();
await mining.$indexDifficultyAdjustments();
await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory();
@@ -191,6 +193,7 @@ class Indexer {
await auditReplicator.$sync();
await statisticsReplicator.$sync();
await AccelerationRepository.$indexPastAccelerations();
await BlocksAuditsRepository.$migrateAuditsV0toV1();
// do not wait for classify blocks to finish
blocks.$classifyBlocks();
} catch (e) {

View File

@@ -29,9 +29,11 @@ export interface PoolStats extends PoolInfo {
}
export interface BlockAudit {
version: number,
time: number,
height: number,
hash: string,
unseenTxs: string[],
missingTxs: string[],
freshTxs: string[],
sigopTxs: string[],
@@ -42,6 +44,19 @@ export interface BlockAudit {
matchRate: number,
expectedFees?: number,
expectedWeight?: number,
template?: any[];
}
export interface TransactionAudit {
seen?: boolean;
expected?: boolean;
added?: boolean;
prioritized?: boolean;
delayed?: number;
accelerated?: boolean;
conflict?: boolean;
coinbase?: boolean;
firstSeen?: number;
}
export interface AuditScore {
@@ -112,6 +127,8 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
};
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
feeDelta?: number;
replacement?: boolean;
uid?: number;
flags?: number;
@@ -209,6 +226,7 @@ export interface CpfpInfo {
sigops?: number;
adjustedVsize?: number,
acceleration?: boolean,
fee?: number;
}
export interface TransactionStripped {
@@ -281,12 +299,14 @@ export interface BlockExtension {
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
name: string;
slug: string;
minerNames: string[] | null;
};
avgFee: number;
avgFeeRate: number;
coinbaseRaw: string;
orphans: OrphanedBlock[] | null;
coinbaseAddress: string | null;
coinbaseAddresses: string[] | null;
coinbaseSignature: string | null;
coinbaseSignatureAscii: string | null;
virtualSize: number;
@@ -300,6 +320,7 @@ export interface BlockExtension {
segwitTotalSize: number;
segwitTotalWeight: number;
header: string;
firstSeen: number | null;
utxoSetChange: number;
// Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null;
@@ -366,8 +387,9 @@ export interface CpfpCluster {
}
export interface CpfpSummary {
transactions: TransactionExtended[];
transactions: MempoolTransactionExtended[];
clusters: CpfpCluster[];
version: number;
}
export interface Statistic {
@@ -433,7 +455,7 @@ export interface OptimizedStatistic {
export interface TxTrackingInfo {
replacedBy?: string,
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[] },
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
cpfp?: {
ancestors?: Ancestor[],
bestDescendant?: Ancestor | null,
@@ -445,6 +467,8 @@ export interface TxTrackingInfo {
utxoSpent?: { [vout: number]: { vin: number, txid: string } },
accelerated?: boolean,
acceleratedBy?: number[],
acceleratedAt?: number,
feeDelta?: number,
confirmed?: boolean
}

View File

@@ -31,11 +31,11 @@ class AuditReplication {
const missingAudits = await this.$getMissingAuditBlocks();
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
let totalSynced = 0;
let totalMissed = 0;
let loggerTimer = Date.now();
// process missing audits in batches of
// process missing audits in batches of BATCH_SIZE
for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
const slice = missingAudits.slice(i, i + BATCH_SIZE);
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
@@ -109,9 +109,11 @@ class AuditReplication {
version: 1,
});
await blocksAuditsRepository.$saveAudit({
version: auditSummary.version || 0,
hash: blockHash,
height: auditSummary.height,
time: auditSummary.timestamp || auditSummary.time,
unseenTxs: auditSummary.unseenTxs || [],
missingTxs: auditSummary.missingTxs || [],
addedTxs: auditSummary.addedTxs || [],
prioritizedTxs: auditSummary.prioritizedTxs || [],

View File

@@ -123,7 +123,7 @@ class StatisticsReplication {
};
const intervals = [ // [start, end, label ]
[now - day, now - 60, '24h'] , // from 24 hours ago to now = 1 minute granularity
[now - day + 600, now - 60, '24h'] , // from 24 hours ago to now = 1 minute granularity
startTime < now - day ? [now - day * 7, now - day, '1w' ] : null, // from 1 week ago to 24 hours ago = 5 minutes granularity
startTime < now - day * 7 ? [now - day * 30, now - day * 7, '1m' ] : null, // from 1 month ago to 1 week ago = 30 minutes granularity
startTime < now - day * 30 ? [now - day * 90, now - day * 30, '3m' ] : null, // from 3 months ago to 1 month ago = 2 hours granularity
@@ -170,15 +170,24 @@ class StatisticsReplication {
return new Set<number>();
}
const roundedTimesAlreadyHere = new Set(rows.map(row => this.roundToNearestStep(row.added, step)));
const missingTimes = new Set(timeSteps.filter(time => !roundedTimesAlreadyHere.has(time)));
const roundedTimesAlreadyHere: number[] = Array.from(new Set(rows.map(row => this.roundToNearestStep(row.added, step))));
const missingTimes = timeSteps.filter(time => !roundedTimesAlreadyHere.includes(time)).filter((time, i, arr) => {
// Remove outsiders
if (i === 0) {
return arr[i + 1] === time + step
} else if (i === arr.length - 1) {
return arr[i - 1] === time - step;
}
return (arr[i + 1] === time + step) && (arr[i - 1] === time - step)
});
// Don't bother fetching if very few rows are missing
if (missingTimes.size < timeSteps.length * 0.005) {
if (missingTimes.length < timeSteps.length * 0.01) {
return new Set();
}
return missingTimes;
return new Set(missingTimes);
} catch (e: any) {
logger.err(`Cannot fetch missing statistics times from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;

View File

@@ -1,4 +1,4 @@
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration';
import { AccelerationInfo } from '../api/acceleration/acceleration';
import { RowDataPacket } from 'mysql2';
import DB from '../database';
import logger from '../logger';
@@ -11,6 +11,7 @@ import accelerationCosts from '../api/acceleration/acceleration';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import transactionUtils from '../api/transaction-utils';
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
import { makeBlockTemplate } from '../api/mini-miner';
export interface PublicAcceleration {
txid: string,
@@ -191,6 +192,7 @@ class AccelerationRepository {
}
}
// modifies block transactions
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
for (const tx of transactions) {
@@ -212,6 +214,15 @@ class AccelerationRepository {
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations);
}
}
let anyConfirmed = false;
for (const acc of accelerations) {
if (blockTxs[acc.txid]) {
anyConfirmed = true;
}
}
if (anyConfirmed) {
accelerationApi.accelerationConfirmed();
}
const lastSyncedHeight = await this.$getLastSyncedHeight();
// if we've missed any blocks, let the indexer catch up from the last synced height on the next run
if (block.height === lastSyncedHeight + 1) {
@@ -308,10 +319,10 @@ class AccelerationRepository {
}
const accelerationSummaries = accelerations.map(acc => ({
...acc,
pools: acc.pools.map(pool => pool.pool_unique_id),
pools: acc.pools,
}))
for (const acc of accelerations) {
if (blockTxs[acc.txid] && acc.pools.some(pool => pool.pool_unique_id === block.extras.pool.id)) {
if (blockTxs[acc.txid] && acc.pools.includes(block.extras.pool.id)) {
const tx = blockTxs[acc.txid];
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));

View File

@@ -1,13 +1,24 @@
import blocks from '../api/blocks';
import DB from '../database';
import logger from '../logger';
import { BlockAudit, AuditScore } from '../mempool.interfaces';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import { BlockAudit, AuditScore, TransactionAudit, TransactionStripped } from '../mempool.interfaces';
interface MigrationAudit {
version: number,
height: number,
id: string,
timestamp: number,
prioritizedTxs: string[],
acceleratedTxs: string[],
template: TransactionStripped[],
transactions: TransactionStripped[],
}
class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> {
try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
@@ -62,24 +73,30 @@ class BlocksAuditRepositories {
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
try {
const [rows]: any[] = await DB.query(
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
template,
missing_txs as missingTxs,
added_txs as addedTxs,
prioritized_txs as prioritizedTxs,
fresh_txs as freshTxs,
sigop_txs as sigopTxs,
fullrbf_txs as fullrbfTxs,
accelerated_txs as acceleratedTxs,
match_rate as matchRate,
expected_fees as expectedFees,
expected_weight as expectedWeight
`SELECT
blocks_audits.version,
blocks_audits.height,
blocks_audits.hash as id,
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
template,
unseen_txs as unseenTxs,
missing_txs as missingTxs,
added_txs as addedTxs,
prioritized_txs as prioritizedTxs,
fresh_txs as freshTxs,
sigop_txs as sigopTxs,
fullrbf_txs as fullrbfTxs,
accelerated_txs as acceleratedTxs,
match_rate as matchRate,
expected_fees as expectedFees,
expected_weight as expectedWeight
FROM blocks_audits
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
WHERE blocks_audits.hash = ?
`, [hash]);
if (rows.length) {
rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs);
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
@@ -98,6 +115,42 @@ class BlocksAuditRepositories {
}
}
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
try {
const blockAudit = await this.$getBlockAudit(hash);
if (blockAudit) {
const isAdded = blockAudit.addedTxs.includes(txid);
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
const isAccelerated = blockAudit.acceleratedTxs.includes(txid);
const isConflict = blockAudit.fullrbfTxs.includes(txid);
let isExpected = false;
let firstSeen = undefined;
blockAudit.template?.forEach(tx => {
if (tx.txid === txid) {
isExpected = true;
firstSeen = tx.time;
}
});
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
return {
seen: wasSeen,
expected: isExpected,
added: isAdded && (blockAudit.version === 0 || !wasSeen),
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
firstSeen,
};
}
return null;
} catch (e: any) {
logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
try {
const [rows]: any[] = await DB.query(
@@ -151,6 +204,96 @@ class BlocksAuditRepositories {
throw e;
}
}
/**
* [INDEXING] Migrate audits from v0 to v1
*/
public async $migrateAuditsV0toV1(): Promise<void> {
try {
let done = false;
let processed = 0;
let lastHeight;
while (!done) {
const [toMigrate]: MigrationAudit[][] = await DB.query(
`SELECT
blocks_audits.height as height,
blocks_audits.hash as id,
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
blocks_summaries.transactions as transactions,
blocks_templates.template as template,
blocks_audits.prioritized_txs as prioritizedTxs,
blocks_audits.accelerated_txs as acceleratedTxs
FROM blocks_audits
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
WHERE blocks_audits.version = 0
AND blocks_summaries.version = 2
ORDER BY blocks_audits.height DESC
LIMIT 100
`) as any[];
if (toMigrate.length <= 0 || lastHeight === toMigrate[0].height) {
done = true;
break;
}
lastHeight = toMigrate[0].height;
logger.info(`migrating ${toMigrate.length} audits to version 1`);
for (const audit of toMigrate) {
// unpack JSON-serialized transaction lists
audit.transactions = JSON.parse((audit.transactions as any as string) || '[]');
audit.template = JSON.parse((audit.template as any as string) || '[]');
// we know transactions in the template, or marked "prioritized" or "accelerated"
// were seen in our mempool before the block was mined.
const isSeen = new Set<string>();
for (const tx of audit.template) {
isSeen.add(tx.txid);
}
for (const txid of audit.prioritizedTxs) {
isSeen.add(txid);
}
for (const txid of audit.acceleratedTxs) {
isSeen.add(txid);
}
const unseenTxs = audit.transactions.slice(0).map(tx => tx.txid).filter(txid => !isSeen.has(txid));
// identify "prioritized" transactions
const prioritizedTxs: string[] = [];
let lastEffectiveRate = 0;
// Iterate over the mined template from bottom to top (excluding the coinbase)
// Transactions should appear in ascending order of mining priority.
for (let i = audit.transactions.length - 1; i > 0; i--) {
const blockTx = audit.transactions[i];
// If a tx has a lower in-band effective fee rate than the previous tx,
// it must have been prioritized out-of-band (in order to have a higher mining priority)
// so exclude from the analysis.
if ((blockTx.rate || 0) < lastEffectiveRate) {
prioritizedTxs.push(blockTx.txid);
} else {
lastEffectiveRate = blockTx.rate || 0;
}
}
// Update audit in the database
await DB.query(`
UPDATE blocks_audits SET
version = ?,
unseen_txs = ?,
prioritized_txs = ?
WHERE hash = ?
`, [1, JSON.stringify(unseenTxs), JSON.stringify(prioritizedTxs), audit.id]);
}
processed += toMigrate.length;
}
logger.info(`migrated ${processed} audits to version 1`);
} catch (e: any) {
logger.err(`Error while migrating audits from v0 to v1. Will try again later. Reason: ` + (e instanceof Error ? e.message : e));
}
}
}
export default new BlocksAuditRepositories();

View File

@@ -5,7 +5,7 @@ import logger from '../logger';
import { Common } from '../api/common';
import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository';
import { RowDataPacket, escape } from 'mysql2';
import { RowDataPacket } from 'mysql2';
import BlocksSummariesRepository from './BlocksSummariesRepository';
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
import bitcoinClient from '../api/bitcoin/bitcoin-client';
@@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips';
import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
interface DatabaseBlock {
id: string;
@@ -40,6 +41,7 @@ interface DatabaseBlock {
avgFeeRate: number;
coinbaseRaw: string;
coinbaseAddress: string;
coinbaseAddresses: string;
coinbaseSignature: string;
coinbaseSignatureAscii: string;
avgTxSize: number;
@@ -55,6 +57,7 @@ interface DatabaseBlock {
utxoSetChange: number;
utxoSetSize: number;
totalInputAmt: number;
firstSeen: number;
}
const BLOCK_DB_FIELDS = `
@@ -82,6 +85,7 @@ const BLOCK_DB_FIELDS = `
blocks.avg_fee_rate AS avgFeeRate,
blocks.coinbase_raw AS coinbaseRaw,
blocks.coinbase_address AS coinbaseAddress,
blocks.coinbase_addresses AS coinbaseAddresses,
blocks.coinbase_signature AS coinbaseSignature,
blocks.coinbase_signature_ascii AS coinbaseSignatureAscii,
blocks.avg_tx_size AS avgTxSize,
@@ -96,7 +100,8 @@ const BLOCK_DB_FIELDS = `
blocks.header,
blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmt
blocks.total_input_amt AS totalInputAmt,
UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen
`;
class BlocksRepository {
@@ -114,7 +119,7 @@ class BlocksRepository {
pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address,
median_timestamp, header, coinbase_address, coinbase_addresses,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
@@ -125,7 +130,7 @@ class BlocksRepository {
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
FROM_UNIXTIME(?), ?, ?,
FROM_UNIXTIME(?), ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
@@ -161,6 +166,7 @@ class BlocksRepository {
block.mediantime,
block.extras.header,
block.extras.coinbaseAddress,
block.extras.coinbaseAddresses ? JSON.stringify(block.extras.coinbaseAddresses) : null,
truncatedCoinbaseSignature,
block.extras.utxoSetSize,
block.extras.utxoSetChange,
@@ -495,7 +501,7 @@ class BlocksRepository {
}
query += ` ORDER BY height DESC
LIMIT 10`;
LIMIT 100`;
try {
const [rows]: any[] = await DB.query(query, params);
@@ -529,7 +535,7 @@ class BlocksRepository {
return null;
}
return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);
return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);
} catch (e) {
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
@@ -922,6 +928,25 @@ class BlocksRepository {
}
}
/**
* Get all indexed blocks with missing coinbase addresses
*/
public async $getBlocksWithoutCoinbaseAddresses(): Promise<any> {
try {
const [blocks] = await DB.query(`
SELECT height, hash, coinbase_addresses
FROM blocks
WHERE coinbase_addresses IS NULL AND
coinbase_address IS NOT NULL
ORDER BY height DESC
`);
return blocks;
} catch (e) {
logger.err(`Cannot get blocks with missing coinbase addresses. Reason: ` + (e instanceof Error ? e.message : e));
return [];
}
}
/**
* Save indexed median fee to avoid recomputing it later
*
@@ -960,6 +985,62 @@ class BlocksRepository {
}
}
/**
* Save coinbase addresses
*
* @param id
* @param addresses
*/
public async $saveCoinbaseAddresses(id: string, addresses: string[]): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET coinbase_addresses = ?
WHERE hash = ?`,
[JSON.stringify(addresses), id]
);
} catch (e) {
logger.err(`Cannot update block coinbase addresses. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Save pool
*
* @param id
* @param poolId
*/
public async $savePool(id: string, poolId: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET pool_id = ?
WHERE hash = ?`,
[poolId, id]
);
} catch (e) {
logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Save block first seen time
*
* @param id
*/
public async $saveFirstSeenTime(id: string, firstSeen: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET first_seen = FROM_UNIXTIME(?)
WHERE hash = ?`,
[firstSeen, id]
);
} catch (e) {
logger.err(`Cannot update block first seen time. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Convert a mysql row block into a BlockExtended. Note that you
* must provide the correct field into dbBlk object param
@@ -994,11 +1075,13 @@ class BlocksRepository {
id: dbBlk.poolId,
name: dbBlk.poolName,
slug: dbBlk.poolSlug,
minerNames: null,
};
extras.avgFee = dbBlk.avgFee;
extras.avgFeeRate = dbBlk.avgFeeRate;
extras.coinbaseRaw = dbBlk.coinbaseRaw;
extras.coinbaseAddress = dbBlk.coinbaseAddress;
extras.coinbaseAddresses = dbBlk.coinbaseAddresses ? JSON.parse(dbBlk.coinbaseAddresses) : [];
extras.coinbaseSignature = dbBlk.coinbaseSignature;
extras.coinbaseSignatureAscii = dbBlk.coinbaseSignatureAscii;
extras.avgTxSize = dbBlk.avgTxSize;
@@ -1015,6 +1098,7 @@ class BlocksRepository {
extras.utxoSetSize = dbBlk.utxoSetSize;
extras.totalInputAmt = dbBlk.totalInputAmt;
extras.virtualSize = dbBlk.weight / 4.0;
extras.firstSeen = dbBlk.firstSeen;
// Re-org can happen after indexing so we need to always get the
// latest state from core
@@ -1045,7 +1129,7 @@ class BlocksRepository {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@@ -1062,6 +1146,10 @@ class BlocksRepository {
}
}
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
blk.extras = <BlockExtension>extras;
return <BlockExtended>blk;
}

View File

@@ -114,6 +114,43 @@ class BlocksSummariesRepository {
return [];
}
public async $getSummariesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> {
try {
const [rows]: any[] = await DB.query(`
SELECT
height,
id,
version
FROM blocks_summaries
WHERE version < ?
ORDER BY height DESC;`, [version]);
return rows;
} catch (e) {
logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e));
}
return [];
}
public async $getTemplatesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> {
try {
const [rows]: any[] = await DB.query(`
SELECT
blocks_summaries.height as height,
blocks_templates.id as id,
blocks_templates.version as version
FROM blocks_templates
JOIN blocks_summaries ON blocks_templates.id = blocks_summaries.id
WHERE blocks_templates.version < ?
ORDER BY height DESC;`, [version]);
return rows;
} catch (e) {
logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e));
}
return [];
}
/**
* Get the fee percentiles if the block has already been indexed, [] otherwise
*

View File

@@ -91,6 +91,26 @@ class CpfpRepository {
return;
}
public async $getClustersAt(height: number): Promise<CpfpCluster[]> {
const [clusterRows]: any = await DB.query(
`
SELECT *
FROM compact_cpfp_clusters
WHERE height = ?
`,
[height]
);
return clusterRows.map(cluster => {
if (cluster?.txs) {
cluster.effectiveFeePerVsize = cluster.fee_rate;
cluster.txs = this.unpack(cluster.txs);
return cluster;
} else {
return null;
}
}).filter(cluster => cluster !== null);
}
public async $deleteClustersFrom(height: number): Promise<void> {
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
try {
@@ -122,6 +142,37 @@ class CpfpRepository {
}
}
public async $deleteClustersAt(height: number): Promise<void> {
logger.info(`Delete cpfp clusters at height ${height} from the database`);
try {
const [rows] = await DB.query(
`
SELECT txs, height, root from compact_cpfp_clusters
WHERE height = ?
`,
[height]
) as RowDataPacket[][];
if (rows?.length) {
for (const clusterToDelete of rows) {
const txs = this.unpack(clusterToDelete?.txs);
for (const tx of txs) {
await transactionRepository.$removeTransaction(tx.txid);
}
}
}
await DB.query(
`
DELETE from compact_cpfp_clusters
WHERE height = ?
`,
[height]
);
} catch (e: any) {
logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
// insert a dummy row to mark that we've indexed as far as this block
public async $insertProgressMarker(height: number): Promise<void> {
try {
@@ -190,6 +241,32 @@ class CpfpRepository {
return [];
}
}
// returns `true` if two sets of CPFP clusters are deeply identical
public compareClusters(clustersA: CpfpCluster[], clustersB: CpfpCluster[]): boolean {
if (clustersA.length !== clustersB.length) {
return false;
}
clustersA = clustersA.sort((a,b) => a.root.localeCompare(b.root));
clustersB = clustersB.sort((a,b) => a.root.localeCompare(b.root));
for (let i = 0; i < clustersA.length; i++) {
if (clustersA[i].root !== clustersB[i].root) {
return false;
}
if (clustersA[i].txs.length !== clustersB[i].txs.length) {
return false;
}
for (let j = 0; j < clustersA[i].txs.length; j++) {
if (clustersA[i].txs[j].txid !== clustersB[i].txs[j].txid) {
return false;
}
}
}
return true;
}
}
export default new CpfpRepository();

View File

@@ -83,6 +83,7 @@ module.exports = {
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
stop: 'stop',
submitBlock: 'submitblock', // bitcoind v0.7.0+
submitPackage: 'submitpackage',
validateAddress: 'validateaddress',
verifyChain: 'verifychain', // bitcoind v0.9.0+
verifyMessage: 'verifymessage',

View File

@@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info';
import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https';
import { Common } from '../api/common';
/**
* Maintain the most recent version of pools-v2.json
*/
class PoolsUpdater {
tag = 'PoolsUpdater';
lastRun: number = 0;
currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
public async $startService(): Promise<void> {
while ('Bitcoin is still alive') {
try {
await this.updatePoolsJson();
} catch (e: any) {
logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag);
}
await Common.sleep$(10000);
}
}
public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false
@@ -23,11 +37,8 @@ class PoolsUpdater {
return;
}
const oneWeek = 604800;
const oneDay = 86400;
const now = new Date().getTime() / 1000;
if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart
if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart
return;
}
@@ -43,26 +54,26 @@ class PoolsUpdater {
this.currentSha = await this.getShaFromDb();
}
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag);
if (this.currentSha !== null && this.currentSha === githubSha) {
return;
}
// See backend README for more details about the mining pools update process
if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool
) {
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_BLOCK_REINDEXING is disabled`);
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`);
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag);
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`, this.tag);
return;
}
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === null) {
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag);
} else {
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag);
}
const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) {
@@ -71,7 +82,7 @@ class PoolsUpdater {
poolsParser.setMiningPools(poolsJson);
if (config.DATABASE.ENABLED === false) { // Don't run db operations
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`);
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag);
return;
}
@@ -81,14 +92,14 @@ class PoolsUpdater {
await this.updateDBSha(githubSha);
await DB.query('COMMIT;');
} catch (e) {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag);
await DB.query('ROLLBACK;');
}
logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
} catch (e) {
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
this.lastRun = now - 600; // Try again in 10 minutes
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag);
}
}
@@ -102,7 +113,7 @@ class PoolsUpdater {
await DB.query('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) {
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
}
}
}
@@ -115,7 +126,7 @@ class PoolsUpdater {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null);
} catch (e) {
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
return null;
}
}
@@ -134,7 +145,7 @@ class PoolsUpdater {
}
}
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag);
return null;
}
@@ -186,7 +197,7 @@ class PoolsUpdater {
}
return data.data;
} catch (e) {
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
retry++;
}
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);

View File

@@ -76,7 +76,7 @@ class FreeCurrencyApi implements ConversionFeed {
}
public async $fetchConversionRates(date: string): Promise<ConversionRates> {
const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`);
const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`, true);
if (response && response['data'] && (response['data'][date] || this.PAID)) {
if (this.PAID) {
response['data'] = this.convertData(response['data']);

View File

@@ -59,7 +59,7 @@ class PriceUpdater {
private currencyConversionFeed: ConversionFeed | undefined;
private newCurrencies: string[] = ['BGN', 'BRL', 'CNY', 'CZK', 'DKK', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR'];
private lastTimeConversionsRatesFetched: number = 0;
private latestConversionsRatesFromFeed: ConversionRates = {};
private latestConversionsRatesFromFeed: ConversionRates = { USD: -1 };
private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
constructor() {
@@ -157,9 +157,9 @@ class PriceUpdater {
try {
this.latestConversionsRatesFromFeed = await this.currencyConversionFeed.$fetchLatestConversionRates();
this.lastTimeConversionsRatesFetched = Math.round(new Date().getTime() / 1000);
logger.debug(`Fetched currencies conversion rates from external API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`);
logger.debug(`Fetched currencies conversion rates from conversions API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`);
} catch (e) {
logger.err(`Cannot fetch conversion rates from the API. Reason: ${(e instanceof Error ? e.message : e)}`);
logger.err(`Cannot fetch conversion rates from conversions API. Reason: ${(e instanceof Error ? e.message : e)}`);
}
}
@@ -408,17 +408,17 @@ class PriceUpdater {
try {
const remainingQuota = await this.currencyConversionFeed?.$getQuota();
if (remainingQuota['month']['remaining'] < 500) { // We need some calls left for the daily updates
logger.debug(`Not enough currency API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining);
logger.debug(`Not enough conversions API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining);
this.additionalCurrenciesHistoryInserted = true; // Do not try again until next day
return;
}
} catch (e) {
logger.err(`Cannot fetch currency API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`);
logger.err(`Cannot fetch conversions API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`);
return;
}
this.additionalCurrenciesHistoryRunning = true;
logger.debug(`Fetching missing conversion rates from external API to fill ${priceTimesToFill.length} rows`, logger.tags.mining);
logger.debug(`Inserting missing historical conversion rates using conversions API to fill ${priceTimesToFill.length} rows`, logger.tags.mining);
let conversionRates: { [timestamp: number]: ConversionRates } = {};
let totalInserted = 0;
@@ -430,10 +430,23 @@ class PriceUpdater {
const month = new Date(priceTime.time * 1000).getMonth();
const yearMonthTimestamp = new Date(year, month, 1).getTime() / 1000;
if (conversionRates[yearMonthTimestamp] === undefined) {
conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01`) || { USD: -1 };
if (conversionRates[yearMonthTimestamp]['USD'] < 0) {
logger.err(`Cannot fetch conversion rates from the API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01. Aborting insertion of missing prices.`, logger.tags.mining);
this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000);
try {
if (year === new Date().getFullYear() && month === new Date().getMonth()) { // For rows in the current month, we use the latest conversion rates
conversionRates[yearMonthTimestamp] = this.latestConversionsRatesFromFeed;
} else {
conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-15`) || { USD: -1 };
}
if (conversionRates[yearMonthTimestamp]['USD'] < 0) {
throw new Error('Incorrect USD conversion rate');
}
} catch (e) {
if ((e instanceof Error ? e.message : '').includes('429')) { // Continue 60 seconds later if and only if error is 429
this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000);
logger.info(`Got a 429 error from conversions API. This is expected to happen a few times during the initial historical price insertion, process will resume in 60 seconds.`, logger.tags.mining);
} else {
logger.err(`Cannot fetch conversion rates from conversions API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01, trying again next day. Error: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
}
break;
}
}

9
backend/src/utils/api.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Request, Response } from 'express';
export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void {
if (req.accepts('json')) {
res.status(statusCode).json({ error: errorMessage });
} else {
res.status(statusCode).send(errorMessage);
}
}

View File

@@ -5,7 +5,7 @@ import config from '../config';
import logger from '../logger';
import * as https from 'https';
export async function query(path): Promise<object | undefined> {
export async function query(path, throwOnFail: boolean = false): Promise<object | undefined> {
type axiosOptions = {
headers: {
'User-Agent': string
@@ -21,6 +21,7 @@ export async function query(path): Promise<object | undefined> {
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
};
let retry = 0;
let lastError: any = null;
while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
try {
@@ -50,6 +51,7 @@ export async function query(path): Promise<object | undefined> {
}
return data.data;
} catch (e) {
lastError = e;
logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e));
retry++;
}
@@ -59,5 +61,10 @@ export async function query(path): Promise<object | undefined> {
}
logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`);
if (throwOnFail && lastError) {
throw lastError;
}
return undefined;
}

View File

@@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opN) {
return;
}
if (!opN.startsWith('OP_PUSHNUM_')) {
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opM) {
return;
}
if (!opM.startsWith('OP_PUSHNUM_')) {
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
@@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number {
} else {
return 9;
}
}
/** Extracts miner names from a DATUM coinbase transaction */
export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null {
let bytes: number[] = [];
for (let c = 0; c < coinbaseRaw.length; c += 2) {
bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16));
}
// Skip block height
let tagLengthByte = 1 + bytes[0];
let tagsLength = bytes[tagLengthByte];
if (tagsLength == 0x4c) {
tagLengthByte += 1;
tagsLength = bytes[tagLengthByte];
}
const tagStart = tagLengthByte + 1;
const tags = bytes.slice(tagStart, tagStart + tagsLength);
let tagString = String.fromCharCode(...tags);
tagString = tagString.replace('\x00', '');
return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, ''));
}

View File

@@ -0,0 +1,58 @@
import * as fs from 'fs';
import logger from '../logger';
import config from '../config';
function readFile(filePath: string, bufferSize?: number): string[] {
const fileSize = fs.statSync(filePath).size;
const chunkSize = bufferSize || fileSize;
const fileDescriptor = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(chunkSize);
fs.readSync(fileDescriptor, buffer, 0, chunkSize, fileSize - chunkSize);
fs.closeSync(fileDescriptor);
const lines = buffer.toString('utf8', 0, chunkSize).split('\n');
return lines;
}
function extractDateFromLogLine(line: string): number | undefined {
// Extract time from log: "2021-08-31T12:34:56Z" or "2021-08-31T12:34:56.123456Z"
const dateMatch = line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{6})?Z/);
if (!dateMatch) {
return undefined;
}
const dateStr = dateMatch[0];
const date = new Date(dateStr);
let timestamp = Math.floor(date.getTime() / 1000); // Remove decimal (microseconds are added later)
const timePart = dateStr.split('T')[1];
const microseconds = timePart.split('.')[1] || '';
if (!microseconds) {
return timestamp;
}
return parseFloat(timestamp + '.' + microseconds);
}
export function getRecentFirstSeen(hash: string): number | undefined {
const debugLogPath = config.CORE_RPC.DEBUG_LOG_PATH;
if (debugLogPath) {
try {
// Read the last few lines of debug.log
const lines = readFile(debugLogPath, 2048);
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (line && line.includes(`Saw new header hash=${hash}`)) {
return extractDateFromLogLine(line);
}
}
} catch (e) {
logger.err(`Cannot parse block first seen time from Core logs. Reason: ` + (e instanceof Error ? e.message : e));
}
}
return undefined;
}

View File

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

3
contributors/jlopp.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 12, 2024.
Signed: jlopp

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 June 18th, 2024.
Signed: mackalex

3
contributors/svrgnty.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 9, 2024.
Signed: svrgnty

View File

@@ -106,9 +106,10 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"EXTERNAL_ASSETS": [],
"STDOUT_LOG_MIN_PRIORITY": "info",
"INDEXING_BLOCKS_AMOUNT": false,
"AUTOMATIC_BLOCK_REINDEXING": false,
"AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6,
@@ -137,9 +138,10 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_EXTERNAL_ASSETS: ""
MEMPOOL_STDOUT_LOG_MIN_PRIORITY: ""
MEMPOOL_INDEXING_BLOCKS_AMOUNT: ""
MEMPOOL_AUTOMATIC_BLOCK_REINDEXING: ""
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
MEMPOOL_POOLS_JSON_URL: ""
MEMPOOL_POOLS_JSON_TREE_URL: ""
MEMPOOL_POOLS_UPDATE_DELAY: ""
MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""

View File

@@ -1,4 +1,4 @@
FROM node:20.13.1-buster-slim AS builder
FROM node:20.15.0-buster-slim AS builder
ARG commitHash
ENV MEMPOOL_COMMIT_HASH=${commitHash}
@@ -24,7 +24,7 @@ RUN npm install --omit=dev --omit=optional
WORKDIR /build
RUN npm run package
FROM node:20.13.1-buster-slim
FROM node:20.15.0-buster-slim
WORKDIR /backend

View File

@@ -25,7 +25,7 @@
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
"GOGGLES_INDEXING": __MEMPOOL_GOGGLES_INDEXING__,
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__,
"AUTOMATIC_POOLS_UPDATE": __MEMPOOL_AUTOMATIC_POOLS_UPDATE__,
"AUDIT": __MEMPOOL_AUDIT__,
"RUST_GBT": __MEMPOOL_RUST_GBT__,
"LIMIT_GBT": __MEMPOOL_LIMIT_GBT__,
@@ -36,6 +36,7 @@
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
},
@@ -46,7 +47,8 @@
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__,
"COOKIE": __CORE_RPC_COOKIE__,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
@@ -60,7 +62,8 @@
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
"FALLBACK": __ESPLORA_FALLBACK__
"FALLBACK": __ESPLORA_FALLBACK__,
"MAX_BEHIND_TIP": __ESPLORA_MAX_BEHIND_TIP__
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
@@ -145,6 +148,10 @@
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
},
"STRATUM": {
"ENABLED": __STRATUM_ENABLED__,
"API": "__STRATUM_API__"
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",

View File

@@ -26,11 +26,12 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
__MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true}
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
@@ -48,6 +49,7 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
# ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -62,6 +64,7 @@ __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
__ESPLORA_MAX_BEHIND_TIP__=${ESPLORA_MAX_BEHIND_TIP:=2}
# SECOND_CORE_RPC
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
@@ -143,12 +146,16 @@ __REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=14819
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# STRATUM
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
# FIAT_PRICE
@@ -183,9 +190,10 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__!${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json
@@ -204,6 +212,7 @@ sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__CORE_RPC_DEBUG_LOG_PATH__!${__CORE_RPC_DEBUG_LOG_PATH__}!g" mempool-config.json
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
@@ -216,6 +225,7 @@ sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTE
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
sed -i "s!__ESPLORA_MAX_BEHIND_TIP__!${__ESPLORA_MAX_BEHIND_TIP__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json
@@ -294,6 +304,10 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
# STRATUM
sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json
sed -i "s!__STRATUM_API__!${__STRATUM_API__}!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

View File

@@ -1,4 +1,4 @@
FROM node:20.13.1-buster-slim AS builder
FROM node:20.15.0-buster-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}
@@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
RUN npm run build
FROM nginx:1.26.0-alpine
FROM nginx:1.27.0-alpine
WORKDIR /patch

View File

@@ -16,7 +16,9 @@ fi
# Runtime overrides - read env vars defined in docker compose
__MAINNET_ENABLED__=${MAINNET_ENABLED:=true}
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
__TESTNET4_ENABLED__=${TESTNET_ENABLED:=false}
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
@@ -28,6 +30,7 @@ __NGINX_PORT__=${NGINX_PORT:=8999}
__BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000}
__MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
__BASE_MODULE__=${BASE_MODULE:=mempool}
__ROOT_NETWORK__=${ROOT_NETWORK:=}
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
@@ -37,12 +40,16 @@ __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}
__ACCELERATOR__=${ACCELERATOR:=false}
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
# Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__
export __TESTNET_ENABLED__
export __TESTNET4_ENABLED__
export __SIGNET_ENABLED__
export __LIQUID_ENABLED__
export __LIQUID_TESTNET_ENABLED__
@@ -54,6 +61,7 @@ export __NGINX_PORT__
export __BLOCK_WEIGHT_UNITS__
export __MEMPOOL_BLOCKS_AMOUNT__
export __BASE_MODULE__
export __ROOT_NETWORK__
export __MEMPOOL_WEBSITE_URL__
export __LIQUID_WEBSITE_URL__
export __MINING_DASHBOARD__
@@ -63,6 +71,8 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
export __ACCELERATOR__
export __ACCELERATOR_BUTTON__
export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__

View File

@@ -35,6 +35,7 @@
"quotes": [1, "single", { "allowTemplateLiterals": true }],
"semi": 1,
"curly": [1, "all"],
"eqeqeq": 1
"eqeqeq": 1,
"no-trailing-spaces": 1
}
}

View File

@@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
### 3. Run the Frontend
_Make sure to use Node.js 16.10 and npm 7._
_Make sure to use Node.js 20.x and npm 9.x or newer._
Install project dependencies and run the frontend server:
@@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
### 1. Build the Frontend
_Make sure to use Node.js 16.10 and npm 7._
_Make sure to use Node.js 20.x and npm 9.x or newer._
Build the frontend:

View File

@@ -54,6 +54,10 @@
"translation": "src/locale/messages.fr.xlf",
"baseHref": "/fr/"
},
"hr": {
"translation": "src/locale/messages.hr.xlf",
"baseHref": "/hr/"
},
"ja": {
"translation": "src/locale/messages.ja.xlf",
"baseHref": "/ja/"

View File

@@ -0,0 +1,48 @@
{
"theme": "wiz",
"enterprise": "bitb",
"branding": {
"name": "bitb",
"title": "BITB",
"site_id": 20,
"header_img": "/resources/bitblogo.svg",
"footer_img": "/resources/bitblogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "BITB"
}
},
{
"component": "goggles",
"mobileOrder": 5
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "BITB",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "BITB"
}
}
]
}
}

View File

@@ -0,0 +1,51 @@
{
"theme": "contrast",
"enterprise": "meta",
"branding": {
"name": "metaplanet",
"title": "Metaplanet",
"site_id": 21,
"header_img": "/resources/metalogo.svg",
"footer_img": "/resources/metalogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "3350"
}
},
{
"component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "Metaplanet_JP"
}
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "3350",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "3350"
}
}
]
}
}

View File

@@ -72,20 +72,6 @@ describe('Liquid', () => {
});
});
it('renders unconfidential addresses correctly on mobile', () => {
cy.viewport('iphone-6');
cy.visit(`${basePath}/address/ex1qqmmjdwrlg59c8q4l75sj6wedjx57tj5grt8pat`);
cy.waitForSkeletonGone();
//TODO: Add proper IDs for these selectors
const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody';
const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)';
cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => {
cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => {
expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth));
});
});
});
describe('peg in/peg out', () => {
it('loads peg in addresses', () => {
cy.visit(`${basePath}/tx/fe764f7bedfc2a37b29d9c8aef67d64a57d253a6b11c5a55555cfd5826483a58`);

View File

@@ -144,13 +144,13 @@ describe('Mainnet', () => {
});
});
['BC1PQYQSZQ', 'bc1PqYqSzQ'].forEach((searchTerm) => {
['BC1PQYQS', 'bc1PqYqS'].forEach((searchTerm) => {
it(`allows searching for partial case insensitive bech32m addresses: ${searchTerm}`, () => {
cy.visit('/');
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
cy.get('app-search-results button.dropdown-item').should('have.length', 1);
cy.get('app-search-results button.dropdown-item').should('have.length', 10);
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
cy.url().should('include', '/address/bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e');
cy.url().should('include', '/address/bc1pqyqs26fs4gnyw4aqttyjqa5ta7075zzfjftyz98qa8vdr49dh7fqm2zkv3');
cy.waitForSkeletonGone();
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
});
@@ -158,13 +158,13 @@ describe('Mainnet', () => {
});
});
['BC1Q000375VXCU', 'bC1q000375vXcU'].forEach((searchTerm) => {
['BC1Q0003', 'bC1q0003'].forEach((searchTerm) => {
it(`allows searching for partial case insensitive bech32 addresses: ${searchTerm}`, () => {
cy.visit('/');
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
cy.get('app-search-results button.dropdown-item').should('have.length', 1);
cy.get('app-search-results button.dropdown-item').should('have.length', 10);
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
cy.url().should('include', '/address/bc1q000375vxcuf5v04lmwy22vy2thvhqkxghgq7dy');
cy.url().should('include', '/address/bc1q000303cgr9zazthut63kdktwtatfe206um8nyh');
cy.waitForSkeletonGone();
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
});
@@ -344,7 +344,9 @@ describe('Mainnet', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.changeNetwork('testnet4');
//TODO(knorrium): add a check for the proxied server
// cy.changeNetwork('testnet4');
cy.changeNetwork('signet');
cy.changeNetwork('mainnet');
});
@@ -543,16 +545,7 @@ describe('Mainnet', () => {
}
});
cy.get('.alert').should('be.visible');
cy.get('.alert').invoke('css', 'width').then((alertWidth) => {
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
});
cy.get('.btn-warning').then(getRectangle).then((rectA) => {
cy.get('.alert').then(getRectangle).then((rectB) => {
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
});
});
cy.get('.alert-replaced').should('be.visible');
});
it('shows RBF transactions properly (desktop)', () => {

View File

@@ -750,7 +750,7 @@
},
"backendInfo": {
"hostname": "node205.tk7.mempool.space",
"version": "3.0.0-dev",
"version": "3.1.0-dev",
"gitCommit": "abbc8a134",
"lightning": false
},

View File

@@ -4,6 +4,7 @@
"SIGNET_ENABLED": false,
"LIQUID_ENABLED": false,
"LIQUID_TESTNET_ENABLED": false,
"MAINNET_ENABLED": true,
"ITEMS_PER_PAGE": 10,
"KEEP_BLOCKS_AMOUNT": 8,
"NGINX_PROTOCOL": "http",
@@ -12,6 +13,7 @@
"BLOCK_WEIGHT_UNITS": 4000000,
"MEMPOOL_BLOCKS_AMOUNT": 8,
"BASE_MODULE": "mempool",
"ROOT_NETWORK": "",
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"MINING_DASHBOARD": true,
@@ -23,5 +25,8 @@
"HISTORICAL_PRICE": true,
"ADDITIONAL_CURRENCIES": false,
"ACCELERATOR": false,
"PUBLIC_ACCELERATIONS": false
"ACCELERATOR_BUTTON": true,
"PUBLIC_ACCELERATIONS": false,
"STRATUM_ENABLED": false,
"SERVICES_API": "https://mempool.space/api/v1/services"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "3.0.0-dev",
"version": "3.1.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -76,9 +76,9 @@
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.5.1",
"@fortawesome/fontawesome-svg-core": "~6.5.1",
"@fortawesome/free-solid-svg-icons": "~6.5.1",
"@fortawesome/fontawesome-common-types": "~6.7.2",
"@fortawesome/fontawesome-svg-core": "~6.7.2",
"@fortawesome/free-solid-svg-icons": "~6.7.2",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0",
@@ -87,15 +87,14 @@
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.21.1",
"esbuild": "^0.24.0",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
"tslib": "~2.8.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
@@ -105,7 +104,7 @@
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0",
"browser-sync": "^3.0.0",
"browser-sync": "^3.0.3",
"http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
@@ -115,7 +114,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.10.0",
"cypress": "^13.17.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@@ -78,6 +78,18 @@ PROXY_CONFIG.push(...[
"^/testnet": ""
},
},
/* Optional proxy to route dev to official acceleration services
{
context: ['/api/v1/services/accelerator/**'],
target: `https://mempool.space/api/v1/services/accelerator/`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/api/v1/services/accelerator": ""
},
},
*/
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,

View File

@@ -3,8 +3,10 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => {
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space';
console.log(`e2e tests running against ${hostname}`);
entry.target = entry.target.replace("mempool.space", hostname);
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
});
module.exports = PROXY_CONFIG;

View File

@@ -1,14 +1,15 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from './app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
import { AddressGroupComponent } from './components/address-group/address-group.component';
import { TrackerComponent } from './components/tracker/tracker.component';
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy'
import { BlockViewComponent } from '@components/block-view/block-view.component';
import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from '@components/clock/clock.component';
import { StatusViewComponent } from '@components/status-view/status-view.component';
import { AddressGroupComponent } from '@components/address-group/address-group.component';
import { TrackerComponent } from '@components/tracker/tracker.component';
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from '@app/route-guards';
const browserWindow = window || {};
// @ts-ignore
@@ -21,16 +22,16 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -44,7 +45,7 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
@@ -59,12 +60,12 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
@@ -82,7 +83,7 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
@@ -102,16 +103,16 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -125,7 +126,7 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
@@ -137,20 +138,22 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: 'tx',
canMatch: [TrackerGuard],
runGuardsAndResolvers: 'always',
loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule),
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'tracker/:id',
component: TrackerComponent,
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -162,19 +165,19 @@ let routes: Routes = [
children: [
{
path: '',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet4',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
{
path: 'signet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
],
},
@@ -209,13 +212,9 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: ''
},
];
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
@@ -226,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -249,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
},
{
path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
@@ -261,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -282,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
children: [
{
path: '',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
],
},
@@ -297,16 +296,19 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
},
{
path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: ''
},
];
}
if (!window['isMempoolSpaceBuild']) {
routes.push({
path: '**',
redirectTo: ''
});
}
@NgModule({
imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabledBlocking',

View File

@@ -151,7 +151,7 @@ export const languages: Language[] = [
{ code: 'fr', name: 'Français' }, // French
// { code: 'gl', name: 'Galego' }, // Galician
{ code: 'ko', name: '한국어' }, // Korean
// { code: 'hr', name: 'Hrvatski' }, // Croatian
{ code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese
@@ -439,4 +439,39 @@ export const fiatCurrencies = {
code: 'ZAR',
indexed: true,
},
};
};
export interface Timezone {
offset: string;
name: string;
}
export const timezones: Timezone[] = [
{ offset: '-12', name: 'Anywhere on Earth (AoE)' },
{ offset: '-11', name: 'Samoa Standard Time (SST)' },
{ offset: '-10', name: 'Hawaii Standard Time (HST)' },
{ offset: '-9', name: 'Alaska Standard Time (AKST)' },
{ offset: '-8', name: 'Pacific Standard Time (PST)' },
{ offset: '-7', name: 'Mountain Standard Time (MST)' },
{ offset: '-6', name: 'Central Standard Time (CST)' },
{ offset: '-5', name: 'Eastern Standard Time (EST)' },
{ offset: '-4', name: 'Atlantic Standard Time (AST)' },
{ offset: '-3', name: 'Argentina Time (ART)' },
{ offset: '-2', name: 'Fernando de Noronha Time (FNT)' },
{ offset: '-1', name: 'Azores Time (AZOT)' },
{ offset: '+0', name: 'Greenwich Mean Time (GMT)' },
{ offset: '+1', name: 'Central European Time (CET)' },
{ offset: '+2', name: 'Eastern European Time (EET)' },
{ offset: '+3', name: 'Moscow Standard Time (MSK)' },
{ offset: '+4', name: 'Armenia Time (AMT)' },
{ offset: '+5', name: 'Pakistan Standard Time (PKT)' },
{ offset: '+6', name: 'Xinjiang Time (XJT)' },
{ offset: '+7', name: 'Indochina Time (ICT)' },
{ offset: '+8', name: 'Hong Kong Time (HKT)' },
{ offset: '+9', name: 'Japan Standard Time (JST)' },
{ offset: '+10', name: 'Australian Eastern Standard Time (AEST)' },
{ offset: '+11', name: 'Norfolk Time (NFT)' },
{ offset: '+12', name: 'New Zealand Standard Time (NZST)' },
{ offset: '+13', name: 'Tonga Time (TOT)' },
{ offset: '+14', name: 'Line Islands Time (LINT)' }
];

View File

@@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from './injection-tokens';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppModule } from './app.module';
import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { ZoneService } from './services/zone.service';
import { AppComponent } from '@components/app/app.component';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { ZoneService } from '@app/services/zone.service';
@NgModule({
@@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service';
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
export class AppServerModule {}

View File

@@ -2,34 +2,38 @@ 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';
import { ZONE_SERVICE } from './injection-tokens';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service';
import { StateService } from './services/state.service';
import { CacheService } from './services/cache.service';
import { PriceService } from './services/price.service';
import { EnterpriseService } from './services/enterprise.service';
import { WebsocketService } from './services/websocket.service';
import { AudioService } from './services/audio.service';
import { PreloadService } from './services/preload.service';
import { SeoService } from './services/seo.service';
import { OpenGraphService } from './services/opengraph.service';
import { ZoneService } from './services/zone-shim.service';
import { SharedModule } from './shared/shared.module';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { LanguageService } from './services/language.service';
import { ThemeService } from './services/theme.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy';
import { ServicesApiServices } from './services/services-api.service';
import { AppComponent } from '@components/app/app.component';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { OrdApiService } from '@app/services/ord-api.service';
import { StateService } from '@app/services/state.service';
import { CacheService } from '@app/services/cache.service';
import { PriceService } from '@app/services/price.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { WebsocketService } from '@app/services/websocket.service';
import { AudioService } from '@app/services/audio.service';
import { PreloadService } from '@app/services/preload.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { ZoneService } from '@app/services/zone-shim.service';
import { SharedModule } from '@app/shared/shared.module';
import { StorageService } from '@app/services/storage.service';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { LanguageService } from '@app/services/language.service';
import { ThemeService } from '@app/services/theme.service';
import { TimeService } from '@app/services/time.service';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
import { ServicesApiServices } from '@app/services/services-api.service';
import { DatePipe } from '@angular/common';
const providers = [
ElectrsApiService,
OrdApiService,
StateService,
CacheService,
PriceService,
@@ -41,10 +45,12 @@ const providers = [
EnterpriseService,
LanguageService,
ThemeService,
TimeService,
ShortenStringPipe,
FiatShortenerPipe,
FiatCurrencyPipe,
CapAddressPipe,
DatePipe,
AppPreloadingStrategy,
ServicesApiServices,
PreloadService,

View File

@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { MasterPageComponent } from '@components/master-page/master-page.component';
const routes: Routes = [
{
path: '',
component: MasterPageComponent,
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule),
data: { preload: true },
}
];

View File

@@ -1,4 +1,5 @@
import { Transaction, Vin } from './interfaces/electrs.interface';
import { Transaction, Vin } from '@interfaces/electrs.interface';
import { Hash } from '@app/shared/sha256';
const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
@@ -70,19 +71,24 @@ export function calcSegwitFeeGains(tx: Transaction) {
}
if (isP2tr) {
if (vin.witness.length === 1) {
// key path spend
// we don't know if this was a multisig or single sig (the goal of taproot :)),
// so calculate fee savings by comparing to the cheapest single sig input type: P2WPKH and say "saved at least ...%"
// the witness size of P2WPKH is 1 (stack size) + 1 (size) + 72 (low s signature) + 1 (size) + 33 (pubkey) = 108 WU
// the witness size of key path P2TR is 1 (stack size) + 1 (size) + 64 (signature) = 66 WU
realizedTaprootGains += 42;
} else {
// script path spend
// complex scripts with multiple spending paths can often be made around 2x to 3x smaller with the Taproot script tree
// because only the hash of the alternative spending path has the be in the witness data, not the entire script,
// but only assumptions can be made because the scripts themselves are unknown (again, the goal of taproot :))
// TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
// every valid taproot input has at least one witness item, however transactions
// created before taproot activation don't need to have any witness data
// (see https://mempool.space/tx/b10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d41)
if (vin.witness?.length) {
if (vin.witness.length === 1) {
// key path spend
// we don't know if this was a multisig or single sig (the goal of taproot :)),
// so calculate fee savings by comparing to the cheapest single sig input type: P2WPKH and say "saved at least ...%"
// the witness size of P2WPKH is 1 (stack size) + 1 (size) + 72 (low s signature) + 1 (size) + 33 (pubkey) = 108 WU
// the witness size of key path P2TR is 1 (stack size) + 1 (size) + 64 (signature) = 66 WU
realizedTaprootGains += 42;
} else {
// script path spend
// complex scripts with multiple spending paths can often be made around 2x to 3x smaller with the Taproot script tree
// because only the hash of the alternative spending path has the be in the witness data, not the entire script,
// but only assumptions can be made because the scripts themselves are unknown (again, the goal of taproot :))
// TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
}
}
} else {
const script = isP2shP2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm;
@@ -129,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
return;
}
const opN = ops.pop();
if (!opN.startsWith('OP_PUSHNUM_')) {
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
@@ -146,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
}
}
const opM = ops.pop();
if (!opM.startsWith('OP_PUSHNUM_')) {
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
@@ -292,9 +298,9 @@ export async function calcScriptHash$(script: string): Promise<string> {
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));
const hash = new Hash().update(buf).digest();
const hashArray = Array.from(new Uint8Array(hash));
return hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
}
}

View File

@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core';
import { EnterpriseService } from '../../services/enterprise.service';
import { EnterpriseService } from '@app/services/enterprise.service';
@Component({
selector: 'app-about-sponsors',

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