Compare commits

...
Author SHA1 Message Date
natsoni b6b3e52436 Shades of green / red on DA blocks 2024-09-02 16:18:23 +02:00
natsoni d04e5128ba Add endpoint for difficulty adjustment by height 2024-09-02 16:16:53 +02: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
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
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
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
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
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 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
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
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
140 changed files with 14327 additions and 10100 deletions

No files matched your search

+7 -5
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>.
+1 -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._
+10 -9
View File
@@ -1,18 +1,19 @@
{
"name": "mempool-backend",
"version": "3.0.0-dev",
"version": "3.1.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-backend",
"version": "3.0.0-dev",
"version": "3.1.0-dev",
"hasInstallScript": true,
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.7.2",
"axios": "~1.7.4",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.19.2",
@@ -2277,9 +2278,9 @@
}
},
"node_modules/axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -9438,9 +9439,9 @@
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
},
"axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
+2 -2
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",
@@ -42,7 +42,7 @@
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.7.2",
"axios": "~1.7.4",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.19.2",
+1 -1
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 });
+20 -7
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,
+1
View File
@@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return'
};
+24 -18
View File
@@ -33,6 +33,7 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -218,10 +219,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),
};
}
@@ -439,7 +440,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
@@ -615,7 +616,7 @@ class Blocks {
// add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
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);
@@ -652,7 +653,7 @@ class Blocks {
}
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;
@@ -904,9 +905,14 @@ class Blocks {
}
}
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, Object.values(mempool.getAccelerations()).map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
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()) {
@@ -927,12 +933,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();
@@ -981,7 +987,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()) {
@@ -1163,7 +1169,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = Common.getTransactionFlags(tx);
flags = Common.getTransactionFlags(tx, height);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -1178,11 +1184,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
@@ -1318,7 +1324,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
@@ -1397,11 +1403,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);
@@ -1413,7 +1419,7 @@ class Blocks {
}
if (transactions?.length != null) {
const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]);
const summary = calculateFastBlockCpfp(height, transactions);
await this.$saveCpfp(hash, height, summary);
+58 -11
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);
@@ -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
}
@@ -335,6 +339,49 @@ export class Common {
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;
@@ -415,7 +462,7 @@ 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)
@@ -548,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;
@@ -564,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));
}
@@ -585,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 {
+3 -1
View File
@@ -6,7 +6,7 @@ import { Acceleration } from './acceleration/acceleration';
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
const MAX_CLUSTER_ITERATIONS = 100;
export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
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
@@ -93,6 +93,7 @@ export function calculateFastBlockCpfp(height: number, transactions: Transaction
return {
transactions,
clusters,
version: 1,
};
}
@@ -159,6 +160,7 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
return {
transactions: transactions.map(tx => txMap[tx.txid]),
clusters: clusterArray,
version: 2,
};
}
+40 -4
View File
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 80;
private static currentVersion = 82;
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);
}
@@ -691,6 +693,18 @@ class DatabaseMigration {
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);
}
}
/**
@@ -1305,6 +1319,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();
+1 -1
View File
@@ -337,7 +337,7 @@ export function makeBlockTemplate(candidates: MempoolTransactionExtended[], acce
let failures = 0;
while (mempoolArray.length || modified.length) {
// skip invalid transactions
while (mempoolArray[0].used || mempoolArray[0].modified) {
while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
mempoolArray.shift();
}
+14 -1
View File
@@ -30,6 +30,7 @@ class MiningRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustment/:height', this.$getDifficultyAdjustment)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlocksHealth)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
@@ -297,6 +298,18 @@ class MiningRoutes {
}
}
private async $getDifficultyAdjustment(req: Request, res: Response) {
try {
const adjustment = await DifficultyAdjustmentsRepository.$getAdjustmentAtHeight(parseInt(req.params.height, 10));
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(adjustment);
} catch (e) {
res.status(e instanceof Error && e.message === 'not found' ? 204 : 500).send(e instanceof Error ? e.message : e);
}
}
private async $getRewardStats(req: Request, res: Response) {
try {
const response = await mining.$getRewardStats(parseInt(req.params.blockCount, 10));
@@ -470,7 +483,7 @@ class MiningRoutes {
res.setHeader('expires', -1);
try {
accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send('ok');
res.status(200).send();
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
+81
View File
@@ -338,6 +338,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();
+7 -3
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';
@@ -933,6 +933,8 @@ class WebsocketHandler {
throw new Error('No WebSocket.Server have been set');
}
const blockTransactions = structuredClone(transactions);
this.printLogs();
await statistics.runStatistics();
@@ -942,7 +944,7 @@ 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);
@@ -962,7 +964,7 @@ class WebsocketHandler {
}
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 : [];
@@ -984,9 +986,11 @@ class WebsocketHandler {
});
BlocksAuditsRepository.$saveAudit({
version: 1,
time: block.timestamp,
height: block.height,
hash: block.id,
unseenTxs: unseen,
addedTxs: added,
prioritizedTxs: prioritized,
missingTxs: censored,
+2
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;
@@ -192,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) {
+4 -1
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[],
@@ -383,8 +385,9 @@ export interface CpfpCluster {
}
export interface CpfpSummary {
transactions: TransactionExtended[];
transactions: MempoolTransactionExtended[];
clusters: CpfpCluster[];
version: number;
}
export interface Statistic {
+4 -2
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 || [],
@@ -192,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) {
@@ -1,13 +1,24 @@
import blocks from '../api/blocks';
import DB from '../database';
import logger from '../logger';
import { BlockAudit, AuditScore, TransactionAudit } 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);
@@ -101,7 +118,7 @@ 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);
@@ -115,16 +132,17 @@ class BlocksAuditRepositories {
firstSeen = tx.time;
}
});
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
return {
seen: isExpected || isPrioritized || isAccelerated,
seen: wasSeen,
expected: isExpected,
added: isAdded,
added: isAdded && (blockAudit.version === 0 || !wasSeen),
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
firstSeen,
}
};
}
return null;
} catch (e: any) {
@@ -186,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();
+1 -1
View File
@@ -1106,7 +1106,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
@@ -88,6 +88,22 @@ class DifficultyAdjustmentsRepository {
}
}
public async $getAdjustmentAtHeight(height: number): Promise<IndexedDifficultyAdjustment> {
try {
if (isNaN(height)) {
throw new Error(`argument must be a number`);
}
const [rows] = await DB.query(`SELECT * FROM difficulty_adjustments WHERE height = ?`, [height]);
if (!rows[0]) {
throw new Error(`not found`);
}
return rows[0] as IndexedDifficultyAdjustment;
} catch (e: any) {
logger.err(`Cannot get difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e;
}
}
public async $getAdjustmentsHeights(): Promise<number[]> {
try {
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
+2 -2
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);
+2 -2
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:
+4
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/"
+1 -10
View File
@@ -543,16 +543,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)', () => {
@@ -750,7 +750,7 @@
},
"backendInfo": {
"hostname": "node205.tk7.mempool.space",
"version": "3.0.0-dev",
"version": "3.1.0-dev",
"gitCommit": "abbc8a134",
"lightning": false
},
+35 -24
View File
@@ -1,12 +1,12 @@
{
"name": "mempool-frontend",
"version": "3.0.0-dev",
"version": "3.1.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-frontend",
"version": "3.0.0-dev",
"version": "3.1.0-dev",
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@angular-devkit/build-angular": "^17.3.1",
@@ -32,6 +32,7 @@
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"cypress": "^13.14.0",
"domino": "^2.1.6",
"echarts": "~5.5.0",
"esbuild": "^0.23.0",
@@ -42,7 +43,7 @@
"rxjs": "~7.8.1",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
"tslib": "~2.7.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
@@ -62,7 +63,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.13.0",
"cypress": "^13.14.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
@@ -699,6 +700,11 @@
"node": ">=10"
}
},
"node_modules/@angular-devkit/build-angular/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/@angular-devkit/build-webpack": {
"version": "0.1703.1",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.1.tgz",
@@ -8040,13 +8046,13 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.13.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
"version": "13.14.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
"integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"@cypress/request": "^3.0.0",
"@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
@@ -8805,9 +8811,9 @@
"integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg=="
},
"node_modules/elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"version": "6.5.7",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
"dependencies": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
@@ -16925,9 +16931,9 @@
}
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
},
"node_modules/tuf-js": {
"version": "2.2.0",
@@ -18849,6 +18855,11 @@
"requires": {
"lru-cache": "^6.0.0"
}
},
"tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
}
}
},
@@ -24127,12 +24138,12 @@
"peer": true
},
"cypress": {
"version": "13.13.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
"version": "13.14.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
"integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.0",
"@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
@@ -24723,9 +24734,9 @@
"integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg=="
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"version": "6.5.7",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
@@ -30763,9 +30774,9 @@
}
},
"tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
},
"tuf-js": {
"version": "2.2.0",
+3 -3
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",
@@ -95,7 +95,7 @@
"esbuild": "^0.23.0",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
"tslib": "~2.7.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
@@ -115,7 +115,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.13.0",
"cypress": "^13.14.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
+1 -1
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
+2 -2
View File
@@ -135,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);
@@ -152,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);
@@ -53,7 +53,7 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76" class="image">
<defs>
<style>
.d {
@@ -125,7 +125,9 @@
<span>Blockstream</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
</svg>
<span>Unchained</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
@@ -150,7 +152,7 @@
<span>Bull Bitcoin</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)">
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
@@ -435,7 +437,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, 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 either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, 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 either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.
@@ -13,8 +13,6 @@
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro {
@@ -158,9 +156,8 @@
margin: 40px 29px 10px;
&.image.coldcard {
border-radius: 0;
width: auto;
max-height: 50px;
margin: 40px 29px 14px 29px;
height: auto;
margin: 20px 29px 20px;
}
}
}
@@ -403,14 +403,14 @@
}
@if (canPayWithApplePay) {
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> }
<div class="paymentMethod mx-2" style="width: 200px; height: 55px">
<img src="/resources/apple-pay.png" height=37 (click)="moveToStep('applepay')">
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('applepay')">
<img src="/resources/apple-pay.png" height=37>
</div>
}
@if (canPayWithGooglePay) {
@if (canPayWithCashapp || canPayWithApplePay) { <span class="mt-1 mb-1"></span> }
<div class="paymentMethod mx-2" style="width: 200px; height: 55px">
<img src="/resources/google-pay.png" height=37 (click)="moveToStep('googlepay')">
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('googlepay')">
<img src="/resources/google-pay.png" height=37>
</div>
}
</div>
@@ -525,7 +525,7 @@
<div class="col-sm">
<div class="d-flex flex-row flex-column justify-content-center align-items-center">
<span i18n="accelerator.confirming-acceleration-with-miners">Confirming your acceleration with our mining pool partners...</span>
@if (timeSincePaid > 20000) {
@if (timeSincePaid > 30000) {
<span i18n="accelerator.confirming-acceleration-with-miners">...sorry, this is taking longer than expected...</span>
}
<div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
@@ -11,6 +11,7 @@ import { MiningStats } from '../../services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service';
import { ApiService } from '../../services/api.service';
import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
@@ -82,6 +83,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
timePaid: number = 0; // time acceleration requested
math = Math;
isMobile: boolean = window.innerWidth <= 767.98;
isProdDomain = ['mempool.space',
'mempool-staging.va1.mempool.space',
'mempool-staging.fmt.mempool.space',
'mempool-staging.fra.mempool.space',
'mempool-staging.tk7.mempool.space',
'mempool-staging.sg1.mempool.space'
].indexOf(document.location.hostname) > -1;
private _step: CheckoutStep = 'summary';
simpleMode: boolean = true;
@@ -204,6 +212,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.fetchEstimate();
}
if (this._step === 'checkout') {
this.insertSquare();
this.enterpriseService.goal(8);
}
if (this._step === 'checkout' && this.canPayWithBitcoin) {
@@ -213,17 +222,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.requestBTCPayInvoice();
} else if (this._step === 'cashapp' && this.cashappEnabled) {
this.loadingCashapp = true;
this.insertSquare();
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'applepay' && this.applePayEnabled) {
this.loadingApplePay = true;
this.insertSquare();
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'googlepay' && this.googlePayEnabled) {
this.loadingGooglePay = true;
this.insertSquare();
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'paid') {
@@ -400,16 +406,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* Square
*/
insertSquare(): void {
if (!this.isProdDomain && !isDevMode()) {
return;
}
if (window['Square']) {
return;
}
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
if (document.location.hostname === 'mempool-staging.fmt.mempool.space' ||
document.location.hostname === 'mempool-staging.va1.mempool.space' ||
document.location.hostname === 'mempool-staging.fra.mempool.space' ||
document.location.hostname === 'mempool-staging.tk7.mempool.space' ||
document.location.hostname === 'mempool.space') {
statsUrl = 'https://web.squarecdn.com/v1/square.js';
if (this.isProdDomain) {
statsUrl = '/square/v1/square.js';
}
(function(): void {
@@ -418,13 +423,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
})();
}
setupSquare(): void {
if (!this.isProdDomain && !isDevMode()) {
return;
}
const init = (): void => {
this.initSquare();
};
if (!window['Square']) {
console.debug('Square.js failed to load properly. Retrying in 1 second.');
setTimeout(init, 1000);
console.debug('Square.js failed to load properly. Retrying.');
setTimeout(this.setupSquare.bind(this), 100);
} else {
init();
}
@@ -656,11 +664,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
amount: costUSD.toFixed(2),
label: 'Total',
pending: true,
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`,
productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
}
});
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`,
redirectURL: `${redirectHostname}/tx/${this.tx.txid}`,
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
});
@@ -797,7 +805,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
get canPayWithCashapp(): boolean {
if (!this.cashappEnabled || !this.conversions) {
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
@@ -813,7 +821,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
get canPayWithApplePay(): boolean {
if (!this.applePayEnabled || !this.conversions) {
if (!this.applePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
@@ -829,7 +837,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
get canPayWithGooglePay(): boolean {
if (!this.googlePayEnabled || !this.conversions) {
if (!this.googlePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
@@ -26,21 +26,21 @@
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
@if (accelerationInfo.status === 'accelerated') {
<td style="color: #905cf4;" class="value">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
} @else {
<td style="color: #905cf4;" class="value">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
}
</tr>
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
@if (accelerationInfo.status === 'seen') {
<td class="label" i18n="transaction.fee-rate">Fee rate</td>
<td class="label" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee" [weight]="accelerationInfo.weight"></app-fee-rate></td>
} @else if (accelerationInfo.status === 'accelerated' || accelerationInfo.status === 'mined') {
<td class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
@if (accelerationInfo.status === 'accelerated') {
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
} @else {
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
}
}
</tr>
@@ -32,4 +32,8 @@
top: -1px;
margin-right: 3px;
}
.oobFees {
color: #905cf4;
}
}
@@ -4,8 +4,8 @@
<div class="clearfix"></div>
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
<div class="acceleration-list">
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
<ng-container *ngIf="pending">
@@ -21,7 +21,7 @@
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
</ng-container>
</thead>
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of accelerations; let i= index;">
<td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
@@ -62,9 +62,9 @@
</td>
<td class="status text-right">
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success" i18n="accelerator.completed">Completed <span *ngIf="acceleration.status === 'completed_provisional'">🔄</span></span>
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success" i18n="accelerator.mined">Mined <span *ngIf="acceleration.status === 'completed_provisional'">🔄</span></span>
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger" i18n="accelerator.canceled">Failed <span *ngIf="acceleration.status === 'failed_provisional'">🔄</span></span>
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;⌛</span></span>
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;⌛</span></span>
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;⌛</span></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@@ -73,22 +73,47 @@
</tr>
</tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="fee text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="fee-delta text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="status text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
</tr>
</tbody>
@if (!pending) {
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 200px"></span>
</td>
<td class="fee text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="block text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="pool text-right" *ngIf="!this.widget">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="status text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
</tr>
</tbody>
} @else {
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="fee-rate text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="bid text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="time text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
</tr>
</tbody>
}
</ng-template>
</table>
@@ -32,6 +32,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
dir: 'rtl' | 'ltr' = 'ltr';
paramSubscription: Subscription;
pools: { [id: number]: SinglePoolStats } = {};
nonEmptyAccelerations: boolean = true;
constructor(
private servicesApiService: ServicesApiServices,
@@ -50,6 +51,12 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.pools[pool.unique_id] = pool;
}
});
if (!this.widget) {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
@@ -82,12 +89,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
).subscribe(() => {
this.pageChange(this.page);
});
this.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.pools[pool.unique_id] = pool;
}
});
}
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
@@ -115,6 +116,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
for (const acc of accelerations) {
acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost;
}
this.nonEmptyAccelerations = accelerations.length > 0;
if (this.widget) {
return of(accelerations.slice(0, 6));
} else {
@@ -1,7 +1,7 @@
@if (chartOnly) {
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
} @else {
<table>
<table style="width: 100%;">
<tbody>
<tr>
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
@@ -11,9 +11,9 @@
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
<app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
</div>
</td>
@@ -61,4 +61,8 @@
& > div, & > div > svg {
overflow: visible !important;
}
}
.oobFees {
color: #905cf4;
}
@@ -67,12 +67,17 @@ export class ActiveAccelerationBox implements OnChanges {
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
// Find the first pool with at least 1% of the total network hashrate
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
data.push(getDataItem(
pool.lastEstimatedHashrate,
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)),
index >= firstSignificantPool
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
: 'white',
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true,
) as PieSeriesOption);
@@ -0,0 +1,5 @@
<div class="sparkles" #sparkleAnchor>
<div *ngFor="let sparkle of sparkles" class="sparkle" [style]="sparkle.style">
<span class="inner-sparkle" [style]="sparkle.rotation">+</span>
</div>
</div>
@@ -0,0 +1,45 @@
.sparkles {
position: absolute;
top: var(--block-size);
height: 50px;
right: 0;
}
.sparkle {
position: absolute;
color: rgba(152, 88, 255, 0.75);
opacity: 0;
transform: scale(0.8) rotate(0deg);
animation: pop ease 2000ms forwards, sparkle ease 500ms infinite;
}
.inner-sparkle {
display: block;
}
@keyframes pop {
0% {
transform: scale(0.8) rotate(0deg);
opacity: 0;
}
20% {
transform: scale(1) rotate(72deg);
opacity: 1;
}
100% {
transform: scale(0) rotate(360deg);
opacity: 0;
}
}
@keyframes sparkle {
0% {
color: rgba(152, 88, 255, 0.75);
}
50% {
color: rgba(198, 162, 255, 0.75);
}
100% {
color: rgba(152, 88, 255, 0.75);
}
}
@@ -0,0 +1,73 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
@Component({
selector: 'app-acceleration-sparkles',
templateUrl: './acceleration-sparkles.component.html',
styleUrls: ['./acceleration-sparkles.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationSparklesComponent implements OnChanges {
@Input() arrow: ElementRef<HTMLDivElement>;
@Input() run: boolean = false;
@ViewChild('sparkleAnchor')
sparkleAnchor: ElementRef<HTMLDivElement>;
constructor(
private cd: ChangeDetectorRef,
) {}
endTimeout: any;
lastSparkle: number = 0;
sparkleWidth: number = 0;
sparkles: any[] = [];
ngOnChanges(changes: SimpleChanges): void {
if (changes.run) {
if (this.endTimeout) {
clearTimeout(this.endTimeout);
this.endTimeout = null;
}
if (this.run) {
this.doSparkle();
} else {
this.endTimeout = setTimeout(() => {
this.sparkles = [];
}, 2000);
}
}
}
doSparkle(): void {
if (this.run) {
const now = performance.now();
if (now - this.lastSparkle > 20) {
this.lastSparkle = now;
if (this.arrow?.nativeElement && this.sparkleAnchor?.nativeElement) {
const anchor = this.sparkleAnchor.nativeElement.getBoundingClientRect().right;
const right = this.arrow.nativeElement.getBoundingClientRect().right;
const dx = (anchor - right) + 30;
const numSparkles = Math.ceil(Math.random() * 3);
for (let i = 0; i < numSparkles; i++) {
this.sparkles.push({
style: {
right: (dx + (Math.random() * 10)) + 'px',
top: (15 + (Math.random() * 30)) + 'px',
},
rotation: {
transform: `rotate(${Math.random() * 360}deg)`,
}
});
}
while (this.sparkles.length > 200) {
this.sparkles.shift();
}
this.cd.markForCheck();
}
}
requestAnimationFrame(() => {
this.doSparkle();
});
}
}
}
@@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
}
handleVin() {
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]);
if (address?.scripts.size) {
const script = address?.scripts.values().next().value;
if (script.template?.label) {
@@ -1,6 +1,5 @@
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles&trade; tooltip" ngbTooltip="select filter categories to highlight matching transactions">
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges float-right" i18n-ngbTooltip="Mempool Goggles&trade; tooltip" ngbTooltip="select filter categories to highlight matching transactions">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
</a>
<div class="filter-bar">
@@ -24,7 +24,6 @@
display: flex;
flex-direction: row;
align-items: center;
float: right;
&:hover, &:active {
text-decoration: none;
@@ -2,10 +2,12 @@
<div class="graph-alignment" [class.grid-align]="!autofit" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
<div class="block-overview-graph">
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div>
@if (!disableSpinner) {
<div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable">
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div>
}
<app-block-overview-tooltip
[tx]="selectedTx || hoverTx"
[cursorPosition]="tooltipPosition"
@@ -18,6 +18,7 @@ const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
};
@@ -25,6 +26,7 @@ const unmatchedContrastAuditColors = {
censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity),
missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity),
added: setOpacity(contrastAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity),
};
@@ -196,7 +198,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
// initialize the scene without any entry transition
setup(transactions: TransactionStripped[]): void {
setup(transactions: TransactionStripped[], sort: boolean = false): void {
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags();
@@ -204,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.filtersAvailable = filtersAvailable;
if (this.scene) {
this.clearUpdateQueue();
this.scene.setup(transactions);
this.scene.setup(transactions, sort);
this.readyNextFrame = true;
this.start();
this.updateSearchHighlight();
@@ -88,16 +88,19 @@ export default class BlockScene {
}
// set up the scene with an initial set of transactions, without any transition animation
setup(txs: TransactionStripped[]) {
setup(txs: TransactionStripped[], sort: boolean = false) {
// clean up any old transactions
Object.values(this.txs).forEach(tx => {
tx.destroy();
delete this.txs[tx.txid];
});
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
txs.forEach(tx => {
const txView = new TxView(tx, this);
this.txs[tx.txid] = txView;
let txViews = txs.map(tx => new TxView(tx, this));
if (sort) {
txViews = txViews.sort(feeRateDescending);
}
txViews.forEach(txView => {
this.txs[txView.txid] = txView;
this.place(txView);
this.saveGridToScreenPosition(txView);
this.applyTxUpdate(txView, {
@@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
flags: number;
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
scene?: BlockScene;
@@ -71,6 +71,7 @@ export const defaultAuditColors = {
censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'),
added_prioritized: darken(desaturate(hexToColor('0099ff'), 0.15), 0.85),
prioritized: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
accelerated: hexToColor('8f5ff6'),
};
@@ -101,6 +102,7 @@ export const contrastAuditColors = {
censored: hexToColor('ffa8ff'),
missing: darken(desaturate(hexToColor('ffa8ff'), 0.3), 0.7),
added: hexToColor('00bb98'),
added_prioritized: darken(desaturate(hexToColor('00bb98'), 0.15), 0.85),
prioritized: darken(desaturate(hexToColor('00bb98'), 0.3), 0.7),
accelerated: hexToColor('8f5ff6'),
};
@@ -136,8 +138,14 @@ export function defaultColorFunction(
return auditColors.missing;
case 'added':
return auditColors.added;
case 'added_prioritized':
return auditColors.added_prioritized;
case 'prioritized':
return auditColors.prioritized;
case 'added_deprioritized':
return auditColors.added_prioritized;
case 'deprioritized':
return auditColors.prioritized;
case 'selected':
return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1];
case 'accelerated':
@@ -51,7 +51,7 @@
<tr *ngIf="hasEffectiveRate && effectiveRate != null">
<td *ngIf="!this.acceleration" class="label" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td *ngIf="this.acceleration" class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
<td class="value">
<td class="value" [class.oobFees]="this.acceleration">
<app-fee-rate [fee]="effectiveRate"></app-fee-rate>
</td>
</tr>
@@ -75,6 +75,15 @@
<span *ngSwitchCase="'freshcpfp'" class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span>
<span *ngSwitchCase="'added'" class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span *ngSwitchCase="'prioritized'" class="badge badge-warning" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
<ng-container *ngSwitchCase="'added_prioritized'">
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
</ng-container>
<span *ngSwitchCase="'deprioritized'" class="badge badge-warning" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
<ng-container *ngSwitchCase="'added_deprioritized'">
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
</ng-container>
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
@@ -27,6 +27,9 @@ th, td {
width: 70%;
text-align: end;
}
&.oobFees {
color: #905cf4;
}
}
.badge.badge-accelerated {
@@ -78,6 +78,25 @@
</ng-template>
</td>
</tr>
<tr *ngIf="this.stateService.network === '' && block.height % 2016 === 0">
<td i18n="mining.difficulty-adjustment">Adjustment</td>
<td>
<ng-container *ngIf="cacheService.daCache[block.height]?.adjustment > 0; else loadingAdjustment">
<div [style.color]="cacheService.daCache[block.height].adjustment > 1 ? 'var(--green)' : (cacheService.daCache[block.height].adjustment < 1 ? 'var(--red)' : '')">
@if (cacheService.daCache[block.height].adjustment > 1) {
<fa-icon class="retarget-sign up" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
} @else if (cacheService.daCache[block.height].adjustment < 1) {
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
}
{{ (cacheService.daCache[block.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
</ng-container>
<ng-template #loadingAdjustment>
<span class="skeleton-loader" style="max-width: 60px"></span>
</ng-template>
</td>
</tr>
</ng-container>
<ng-template #skeletonRows>
<tr>
@@ -193,6 +212,10 @@
</span>
</td>
</tr>
<tr *ngIf="this.stateService.network === '' && block.height % 2016 === 0">
<td i18n="block.difficulty">Difficulty</td>
<td>{{ block.difficulty | amountShortener: 2 }}</td>
</tr>
</ng-container>
<ng-template #loadingRest>
<tr>
@@ -280,3 +280,11 @@ h1 {
top: -1px;
margin-right: 2px;
}
.retarget-sign {
margin-right: -3px;
&.up {
position: relative;
top: 2px;
}
}
@@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service';
import { ServicesApiServices } from '../../services/services-api.service';
import { PreloadService } from '../../services/preload.service';
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
@Component({
selector: 'app-block',
@@ -98,7 +99,7 @@ export class BlockComponent implements OnInit, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe,
private apiService: ApiService,
private priceService: PriceService,
private cacheService: CacheService,
public cacheService: CacheService,
private servicesApiService: ServicesApiServices,
private cd: ChangeDetectorRef,
private preloadService: PreloadService,
@@ -521,8 +522,10 @@ export class BlockComponent implements OnInit, OnDestroy {
if (transactions && blockAudit) {
const inTemplate = {};
const inBlock = {};
const isUnseen = {};
const isAdded = {};
const isPrioritized = {};
const isDeprioritized = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
@@ -534,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy {
this.numUnexpected = 0;
if (blockAudit?.template) {
// augment with locally calculated *de*prioritized transactions if possible
const { prioritized, deprioritized } = identifyPrioritizedTransactions(transactions);
// but if the local calculation produces returns unexpected results, don't use it
let useLocalDeprioritized = deprioritized.length < (transactions.length * 0.1);
for (const tx of prioritized) {
if (!isPrioritized[tx] && !isAccelerated[tx]) {
useLocalDeprioritized = false;
break;
}
}
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
if (tx.acc) {
@@ -543,12 +557,20 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.unseenTxs || []) {
isUnseen[txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.prioritizedTxs || []) {
for (const txid of blockAudit.prioritizedTxs) {
isPrioritized[txid] = true;
}
if (useLocalDeprioritized) {
for (const txid of deprioritized || []) {
isDeprioritized[txid] = true;
}
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
@@ -592,18 +614,33 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = 'accelerated';
}
}
for (const [index, tx] of transactions.entries()) {
let anySeen = false;
for (let index = transactions.length - 1; index >= 0; index--) {
const tx = transactions[index];
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (isPrioritized[tx.txid]) {
tx.status = 'prioritized';
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
tx.status = 'added_prioritized';
} else {
tx.status = 'prioritized';
}
} else if (isDeprioritized[tx.txid]) {
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
tx.status = 'added_deprioritized';
} else {
tx.status = 'deprioritized';
}
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
anySeen = true;
tx.status = 'found';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else if (isUnseen[tx.txid] && anySeen) {
tx.status = 'added';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
@@ -1,10 +1,12 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Observable, Subscription, delay, filter, tap } from 'rxjs';
import { Observable, Subscription, delay, filter, of, retryWhen, switchMap, take, tap, throwError } from 'rxjs';
import { StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { Location } from '@angular/common';
import { CacheService } from '../../services/cache.service';
import { ApiService } from '../../services/api.service';
import { colorFromRetarget } from '../../shared/common.utils';
interface BlockchainBlock extends BlockExtended {
placeholder?: boolean;
@@ -77,6 +79,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
constructor(
public stateService: StateService,
public cacheService: CacheService,
public apiService: ApiService,
private cd: ChangeDetectorRef,
private location: Location,
) {
@@ -334,6 +337,31 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return this.specialBlocks[height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
}
isDA(height: number): boolean {
const isDA = height % 2016 === 0 && this.stateService.network === '';
if (isDA && !this.cacheService.daCache[height]?.exact) {
const estimatedAdjustment = this.cacheService.daCache[height]?.adjustment || 0;
this.cacheService.daCache[height] = { adjustment: estimatedAdjustment, exact: true };
this.apiService.getDifficultyAdjustmentByHeight$(height).pipe(
switchMap(da => {
const blocksAvailable = (this.height || this.chainTip) && this.blockStyles[(this.height || this.chainTip) - height];
return blocksAvailable ? of(da) : throwError(() => new Error());
}),
retryWhen(errors =>
errors.pipe(
delay(1000),
take(3)
)
),
tap((da) => {
this.cacheService.daCache[height] = { adjustment: da?.adjustment || 1, exact: true };
this.blockStyles[(this.height || this.chainTip) - height].background = colorFromRetarget(da?.adjustment);
})
).subscribe();
}
return isDA;
}
getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) {
if (!block || block.placeholder) {
return this.getStyleForPlaceholderBlock(index, animateEnterFrom);
@@ -349,7 +377,8 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
left: addLeft + this.blockOffset * index + 'px',
background: `repeating-linear-gradient(
background: this.isDA(block.height) ? colorFromRetarget(this.cacheService.daCache[block.height]?.adjustment || 1) :
`repeating-linear-gradient(
var(--secondary),
var(--secondary) ${greenBackgroundHeight}%,
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,
@@ -5,6 +5,7 @@ import { ApiService } from '../../services/api.service';
import { formatNumber } from '@angular/common';
import { selectPowerOfTen } from '../../bitcoin.utils';
import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
@Component({
selector: 'app-difficulty-adjustments-table',
@@ -27,7 +28,8 @@ export class DifficultyAdjustmentsTable implements OnInit {
constructor(
@Inject(LOCALE_ID) public locale: string,
private apiService: ApiService,
public stateService: StateService
public stateService: StateService,
private cacheService: CacheService,
) {
}
@@ -52,6 +54,7 @@ export class DifficultyAdjustmentsTable implements OnInit {
adjustment[2] / selectedPowerOfTen.divider,
this.locale, `1.${decimals}-${decimals}`) + selectedPowerOfTen.unit
});
this.cacheService.daCache[adjustment[1]] = { adjustment: adjustment[3], exact: true };
}
this.isLoading = false;
return tableData.slice(0, 6);
@@ -52,7 +52,7 @@
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet4'] || '/testnet4')" ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet4'] || '/testnet4')" ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
@@ -70,7 +70,7 @@
<a ngbDropdownItem *ngIf="env.MAINNET_ENABLED" class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet4" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet4" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
</div>
@@ -31,7 +31,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
lastBlockHeight: number;
blockIndex: number;
isLoading$ = new BehaviorSubject<boolean>(true);
isLoading$ = new BehaviorSubject<boolean>(false);
timeLtrSubscription: Subscription;
timeLtr: boolean;
chainDirection: string = 'right';
@@ -95,6 +95,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
}
}
this.updateBlock({
block: this.blockIndex,
removed,
changed,
added
@@ -110,8 +111,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
if (this.blockGraph) {
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
}
this.isLoading$.next(true);
this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
if (!this.websocketService.startTrackMempoolBlock(changes.index.currentValue) && this.stateService.mempoolBlockState && this.stateService.mempoolBlockState.block === changes.index.currentValue) {
this.resumeBlock(Object.values(this.stateService.mempoolBlockState.transactions));
} else {
this.isLoading$.next(true);
}
}
}
@@ -153,6 +157,19 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
this.isLoading$.next(false);
}
resumeBlock(transactionsStripped: TransactionStripped[]): void {
if (this.blockGraph) {
this.firstLoad = false;
this.blockGraph.setup(transactionsStripped, true);
this.blockIndex = this.index;
this.isLoading$.next(false);
} else {
requestAnimationFrame(() => {
this.resumeBlock(transactionsStripped);
});
}
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
@@ -41,6 +41,25 @@
</div>
</td>
</tr>
<tr *ngIf="this.stateService.network === '' && mempoolBlock.height % 2016 === 0">
<td i18n="mining.difficulty-adjustment">Adjustment</td>
<td>
<ng-container *ngIf="cacheService.daCache[mempoolBlock.height]?.adjustment > 0; else loadingAdjustment">
<div [style.color]="cacheService.daCache[mempoolBlock.height].adjustment > 1 ? 'var(--green)' : (cacheService.daCache[mempoolBlock.height].adjustment < 1 ? 'var(--red)' : '')">
@if (cacheService.daCache[mempoolBlock.height].adjustment > 1) {
<fa-icon class="retarget-sign up" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
} @else if (cacheService.daCache[mempoolBlock.height].adjustment < 1) {
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
}
{{ (cacheService.daCache[mempoolBlock.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
</ng-container>
<ng-template #loadingAdjustment>
<span class="skeleton-loader" style="max-width: 60px"></span>
</ng-template>
</td>
</tr>
</tbody>
</table>
<app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
@@ -36,3 +36,11 @@ h1 {
margin: auto;
}
}
.retarget-sign {
margin-right: -3px;
&.up {
position: relative;
top: 2px;
}
}
@@ -9,6 +9,7 @@ import { Observable, BehaviorSubject } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { WebsocketService } from '../../services/websocket.service';
import { CacheService } from '../../services/cache.service';
@Component({
selector: 'app-mempool-block',
@@ -30,6 +31,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
public stateService: StateService,
private seoService: SeoService,
private websocketService: WebsocketService,
public cacheService: CacheService,
private cd: ChangeDetectorRef,
@Inject(PLATFORM_ID) private platformId: Object,
) {
@@ -71,7 +73,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
})
);
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap)));
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(({transactions}) => Object.values(transactions)));
this.network$ = this.stateService.networkChanged$;
}
@@ -51,7 +51,8 @@
</div>
</ng-template>
</div>
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + (blockWidth * 0.3) + containerOffset + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
<app-acceleration-sparkles [style]="{ position: 'absolute', right: 0}" [arrow]="arrowElement" [run]="acceleratingArrow"></app-acceleration-sparkles>
<div *ngIf="arrowVisible" #arrowUp id="arrow-up" [ngStyle]="{'right': rightPosition + (blockWidth * 0.3) + containerOffset + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
</div>
</ng-container>
@@ -1,10 +1,10 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
import { Subscription, Observable, of, combineLatest } from 'rxjs';
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
import { Subscription, Observable, of, combineLatest, throwError } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { EtaService } from '../../services/eta.service';
import { Router } from '@angular/router';
import { delay, filter, map, switchMap, tap } from 'rxjs/operators';
import { delay, filter, map, retryWhen, switchMap, take, tap } from 'rxjs/operators';
import { feeLevels } from '../../app.constants';
import { specialBlocks } from '../../app.constants';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@@ -12,6 +12,8 @@ import { Location } from '@angular/common';
import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface';
import { animate, style, transition, trigger } from '@angular/animations';
import { ThemeService } from '../../services/theme.service';
import { CacheService } from '../../services/cache.service';
import { colorFromRetarget } from '../../shared/common.utils';
@Component({
selector: 'app-mempool-blocks',
@@ -77,6 +79,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
maxArrowPosition = 0;
rightPosition = 0;
transition = 'background 2s, right 2s, transform 1s';
@ViewChild('arrowUp')
arrowElement: ElementRef<HTMLDivElement>;
acceleratingArrow: boolean = false;
markIndex: number;
txPosition: MempoolPosition;
@@ -90,6 +95,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
constructor(
private router: Router,
public stateService: StateService,
public cacheService: CacheService,
private etaService: EtaService,
private themeService: ThemeService,
private cd: ChangeDetectorRef,
@@ -201,6 +207,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.markBlocksSubscription = this.stateService.markBlock$
.subscribe((state) => {
const oldTxPosition = this.txPosition;
this.markIndex = undefined;
this.txPosition = undefined;
this.txFeePerVSize = undefined;
@@ -209,6 +216,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
if (state.mempoolPosition) {
this.txPosition = state.mempoolPosition;
if (this.txPosition.accelerated && !oldTxPosition?.accelerated) {
this.acceleratingArrow = true;
setTimeout(() => {
this.acceleratingArrow = false;
}, 2000);
}
}
if (state.txFeePerVSize) {
this.txFeePerVSize = state.txFeePerVSize;
@@ -377,6 +390,37 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.mempoolBlocksFull.forEach((block, i) => this.mempoolBlockStyles.push(this.getStyleForMempoolBlock(block, i)));
}
isDA(height: number): boolean {
if (this.chainTip === -1) {
return false;
}
const isDA = height % 2016 === 0 && this.stateService.network === '';
if (isDA && !this.cacheService.daCache[height]) {
this.cacheService.daCache[height] = { adjustment: 0 };
this.difficultyAdjustments$.pipe(
filter(da => !!da),
switchMap(da => {
const mempoolBlocksAvailable = this.chainTip && this.mempoolBlockStyles[height - this.chainTip - 1];
return mempoolBlocksAvailable ? of(da) : throwError(() => new Error());
}),
retryWhen(errors =>
errors.pipe(
delay(100),
take(3)
)
),
tap(da => {
const adjustment = parseFloat((1 + da.difficultyChange / 100).toFixed(4));
if (adjustment !== this.cacheService.daCache[height].adjustment) {
this.cacheService.daCache[height].adjustment = adjustment;
this.mempoolBlockStyles[height - this.chainTip - 1].background = colorFromRetarget(adjustment);
}
})
).subscribe();
}
return isDA;
}
getStyleForMempoolBlock(mempoolBlock: MempoolBlock, index: number) {
const emptyBackgroundSpacePercentage = Math.max(100 - mempoolBlock.blockVSize / this.stateService.blockVSize * 100, 0);
const usedBlockSpace = 100 - emptyBackgroundSpacePercentage;
@@ -400,7 +444,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
'right': this.containerOffset + index * this.blockOffset + 'px',
'background': backgroundGradients.join(',') + ')'
'background': this.isDA(mempoolBlock.height) ? colorFromRetarget(this.cacheService.daCache[mempoolBlock.height]?.adjustment || 1) : backgroundGradients.join(',') + ')'
};
}
@@ -46,7 +46,7 @@
@if (replaced) {
<div class="alert-replaced" role="alert">
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
<app-truncate [text]="latestReplacement" [lastChars]="12" [link]="['/tracker/' | relativeUrl, latestReplacement]"></app-truncate>
<app-truncate [text]="latestReplacement" [lastChars]="12" [link]="['/tx/' | relativeUrl, latestReplacement]" [queryParams]="{mode: 'status'}"></app-truncate>
</div>
} @else {
<div class="tracker-bar">
@@ -61,7 +61,7 @@
@if (transactionTime > 0) {
<i><app-time kind="since" [time]="transactionTime" [fastRender]="true" [showTooltip]="true"></app-time></i>
} @else {
<span class="skeleton-loader" style="max-width: 50%;"></span>
<span class="skeleton-loader" style="max-width: 200px;"></span>
}
</div>
</div>
@@ -78,7 +78,7 @@
</span>
</ng-container>
<ng-template #etaSkeleton>
<span class="skeleton-loader"></span>
<span class="skeleton-loader" style="max-width: 200px;"></span>
</ng-template>
</div>
</div>
@@ -118,7 +118,7 @@
</div>
<span class="explainer">&nbsp;</span>
} @else {
@if (!tx.status?.confirmed && showAccelerationSummary) {
@if (tx && !tx.status?.confirmed && showAccelerationSummary) {
<ng-container *ngIf="(ETA$ | async) as eta;">
<app-accelerate-checkout
*ngIf="(da$ | async) as da;"
@@ -135,7 +135,7 @@
></app-accelerate-checkout>
</ng-container>
}
<div class="status-panel d-flex flex-column h-100 w-100 justify-content-center align-items-center" [class.small-status]="!tx.status?.confirmed && showAccelerationSummary">
<div class="status-panel d-flex flex-column h-100 w-100 justify-content-center align-items-center" [class.small-status]="tx && !tx.status?.confirmed && showAccelerationSummary">
@if (tx?.acceleration && !tx.status?.confirmed) {
<div class="progress-icon">
<fa-icon [icon]="['fas', 'wand-magic-sparkles']" [fixedWidth]="true"></fa-icon>
@@ -186,7 +186,7 @@
</div>
<div class="footer-link"
[routerLink]="['/tx' | relativeUrl, tx?.txid]"
[routerLink]="['/tx' | relativeUrl, tx?.txid || txId]"
[queryParams]="{ mode: 'details' }"
queryParamsHandling="merge"
>
@@ -293,7 +293,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
})
).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;
@@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
checkAccelerationEligibility() {
if (this.tx) {
this.tx.flags = getTransactionFlags(this.tx);
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
const highSigop = (this.tx.sigops * 20) > this.tx.weight;
this.eligibleForAcceleration = !replaceableInputs && !highSigop;
@@ -8,7 +8,7 @@
<div *ngIf="officialMempoolSpace">
<h2>Trademark Policy and Guidelines</h2>
<h5>The Mempool Open Source Project &reg;</h5>
<h6>Updated: July 3, 2024</h6>
<h6>Updated: August 19, 2024</h6>
<br>
<div class="text-left">
@@ -100,11 +100,26 @@
<p>The Mempool Accelerator Logo</p>
<br><br>
<img src="/resources/mempool-research.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool research Logo</p>
<br><br>
<app-svg-images name="goggles" height="96px"></app-svg-images>
<br><br>
<p>The Mempool Goggles Logo</p>
<br><br>
<img src="/resources/mempool-transaction.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool transaction Logo</p>
<br><br>
<img src="/resources/mempool-block-visualization.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool block visualization Logo</p>
<br><br>
<img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool Blocks Logo</p>
@@ -606,7 +606,12 @@
@if (!isLoadingTx) {
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></td>
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
}
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
</td>
</tr>
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
@@ -642,9 +647,9 @@
<td>
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) {
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
@if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) {
@@ -332,3 +332,7 @@
top: -1px;
margin-right: 2px;
}
.oobFees {
color: #905cf4;
}
@@ -358,12 +358,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}),
).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;
this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
if (acceleration.txid === this.txId) {
if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') {
if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;
this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
} else {
this.tx.feeDelta = undefined;
}
}
this.waitingForAccelerationInfo = false;
this.setIsAccelerated();
}
@@ -411,10 +417,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
const isConflict = audit.fullrbfTxs.includes(txid);
const isExpected = audit.template.some(tx => tx.txid === txid);
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
const wasSeen = audit.version === 1 ? !audit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
return {
seen: isExpected || isPrioritized || isAccelerated,
seen: wasSeen,
expected: isExpected,
added: isAdded,
added: isAdded && (audit.version === 0 || !wasSeen),
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
@@ -461,9 +468,23 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (txPosition.position.acceleratedBy) {
txPosition.cpfp.acceleratedBy = txPosition.position.acceleratedBy;
}
if (txPosition.position.acceleratedAt) {
txPosition.cpfp.acceleratedAt = txPosition.position.acceleratedAt;
}
if (txPosition.position.feeDelta) {
txPosition.cpfp.feeDelta = txPosition.position.feeDelta;
}
this.setCpfpInfo(txPosition.cpfp);
} else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) {
this.tx.acceleratedBy = txPosition.position.acceleratedBy;
} else if ((this.tx?.acceleration)) {
if (txPosition.position.acceleratedBy) {
this.tx.acceleratedBy = txPosition.position.acceleratedBy;
}
if (txPosition.position.acceleratedAt) {
this.tx.acceleratedAt = txPosition.position.acceleratedAt;
}
if (txPosition.position.feeDelta) {
this.tx.feeDelta = txPosition.position.feeDelta;
}
}
if (this.stateService.network === '') {
@@ -523,6 +544,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
queryParamsHandling: 'merge',
preserveFragment: true,
queryParams: { mode: 'details' },
replaceUrl: true,
});
}
this.seoService.setTitle(
@@ -879,7 +901,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
this.tx.flags = getTransactionFlags(this.tx);
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
this.checkAccelerationEligibility();
} else {
@@ -43,7 +43,7 @@
<span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
<span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span>
</ng-container>
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
<span *ngIf="line.type !== 'fee'"> #{{ line.index }}</span>
<ng-container [ngSwitch]="line.type">
<span *ngSwitchCase="'input'">
<ng-container *ngIf="line.status?.block_height">
@@ -73,7 +73,7 @@
<app-truncate [text]="line.txid"></app-truncate>
</p>
<ng-container [ngSwitch]="line.type">
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout + 1 }}
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout }}
<ng-container *ngIf="line.status?.block_height">
<ng-container *ngIf="line.blockHeight; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: line.blockHeight - line?.status?.block_height, connector: true}"></ng-container>
@@ -83,7 +83,7 @@
</ng-template>
</ng-container>
</p>
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin + 1 }}
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin }}
<ng-container *ngIf="line.blockHeight">
<ng-container *ngIf="line?.status?.block_height; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksLater; context:{n: line?.status?.block_height - line.blockHeight, connector: true}"></ng-container>
@@ -3670,6 +3670,39 @@ export const restApiDocsData = [
}
}
},
{
type: "endpoint",
category: "mining",
httpRequestMethod: "GET",
fragment: "get-difficulty-adjustment-by-height",
title: "GET Difficulty Adjustment",
description: {
default: "<p>Returns difficulty adjustment for the block at the specified <code>:blockHeight</code>. If no adjustment happened at that height, an empty response is returned.</p>"
},
urlString: "/v1/mining/difficulty-adjustment/:blockHeight",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `/api/v1/mining/difficulty-adjustment/%{1}`,
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: [`756000`],
response: `{
"time": "2022-09-28T02:56:34.000Z",
"height": 756000,
"difficulty": 31360548173144.9,
"adjustment": 0.97863
}`
},
}
}
},
{
type: "endpoint",
category: "mining",
@@ -211,6 +211,8 @@ export interface BlockExtended extends Block {
}
export interface BlockAudit extends BlockExtended {
version: number,
unseenTxs?: string[],
missingTxs: string[],
addedTxs: string[],
prioritizedTxs: string[],
@@ -237,7 +239,7 @@ export interface TransactionStripped {
acc?: boolean;
flags?: number | null;
time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
}
@@ -251,6 +253,8 @@ export interface MempoolPosition {
vsize: number,
accelerated?: boolean,
acceleratedBy?: number[],
acceleratedAt?: number,
feeDelta?: number,
}
export interface AccelerationPosition extends MempoolPosition {
@@ -72,11 +72,13 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
}
export interface MempoolBlockDelta {
block: number;
added: TransactionStripped[];
removed: string[];
changed: { txid: string, rate: number, flags: number, acc: boolean }[];
}
export interface MempoolBlockState {
block: number;
transactions: TransactionStripped[];
}
export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState;
+1 -1
View File
@@ -13,7 +13,7 @@ class GuardService {
trackerGuard(route: Route, segments: UrlSegment[]): boolean {
const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
return preferredRoute !== 'details' && this.navigationService.isInitialLoad() && window.innerWidth <= 767.98;
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98;
}
}
+6
View File
@@ -315,6 +315,12 @@ export class ApiService {
);
}
getDifficultyAdjustmentByHeight$(height: number): Observable<any> {
return this.httpClient.get<any>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustment/${height}`
);
}
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
@@ -23,6 +23,7 @@ export class CacheService {
blockLoading: { [height: number]: boolean } = {};
copiesInBlockQueue: { [height: number]: number } = {};
blockPriorities: number[] = [];
daCache: { [height: number]: { adjustment: number, exact?: boolean } } = {};
constructor(
private stateService: StateService,
@@ -128,6 +129,7 @@ export class CacheService {
this.blockLoading = {};
this.copiesInBlockQueue = {};
this.blockPriorities = [];
this.daCache = {};
}
getCachedBlock(height) {
@@ -30,7 +30,6 @@ export class EnterpriseService {
this.fetchSubdomainInfo();
this.disableSubnetworks();
this.stateService.env.ACCELERATOR = false;
this.stateService.env.ACCELERATOR_BUTTON = false;
} else {
this.insertMatomo();
}
+10 -1
View File
@@ -37,7 +37,11 @@ export class MiningService {
private stateService: StateService,
private apiService: ApiService,
private storageService: StorageService,
) { }
) {
this.stateService.networkChanged$.subscribe((network) => {
this.clearCache();
});
}
/**
* Generate pool ranking stats
@@ -141,4 +145,9 @@ export class MiningService {
totalBlockCount: parseInt(response.headers.get('x-total-count'), 10),
};
}
private clearCache(): void {
this.cache = {};
this.poolsData = [];
}
}
+27 -15
View File
@@ -5,7 +5,7 @@ import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, Mempool
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { filter, map, scan, shareReplay } from 'rxjs/operators';
import { filter, map, scan, share, shareReplay } from 'rxjs/operators';
import { StorageService } from './storage.service';
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
import { ActiveFilter } from '../shared/filters.utils';
@@ -131,6 +131,7 @@ export class StateService {
latestBlockHeight = -1;
blocks: BlockExtended[] = [];
mempoolSequence: number;
mempoolBlockState: { block: number, transactions: { [txid: string]: TransactionStripped} };
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
networkChanged$ = new ReplaySubject<string>(1);
@@ -143,7 +144,7 @@ export class StateService {
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>();
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
accelerations$ = new Subject<AccelerationDelta>();
liveAccelerations$: Observable<Acceleration[]>;
txConfirmed$ = new Subject<[string, BlockExtended]>();
@@ -210,6 +211,10 @@ export class StateService {
this.env.MINING_DASHBOARD = false;
}
if (document.location.hostname.endsWith('.onion')) {
this.env.SERVICES_API = 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/services';
}
if (this.isBrowser) {
this.setNetworkBasedonUrl(window.location.pathname);
this.setLightningBasedonUrl(window.location.pathname);
@@ -227,33 +232,40 @@ export class StateService {
}
});
if (this.referrer === 'https://cash.app/' && window.innerWidth < 850 && window.location.pathname.startsWith('/tx/')) {
this.router.navigate(['/tracker/' + window.location.pathname.slice(4)]);
}
this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => {
this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((acc: { block: number, transactions: { [txid: string]: TransactionStripped } }, change: MempoolBlockUpdate): { block: number, transactions: { [txid: string]: TransactionStripped } } => {
if (isMempoolState(change)) {
const txMap = {};
change.transactions.forEach(tx => {
txMap[tx.txid] = tx;
});
return txMap;
this.mempoolBlockState = {
block: change.block,
transactions: txMap
};
return this.mempoolBlockState;
} else {
change.added.forEach(tx => {
transactions[tx.txid] = tx;
acc.transactions[tx.txid] = tx;
});
change.removed.forEach(txid => {
delete transactions[txid];
delete acc.transactions[txid];
});
change.changed.forEach(tx => {
if (transactions[tx.txid]) {
transactions[tx.txid].rate = tx.rate;
transactions[tx.txid].acc = tx.acc;
if (acc.transactions[tx.txid]) {
acc.transactions[tx.txid].rate = tx.rate;
acc.transactions[tx.txid].acc = tx.acc;
}
});
return transactions;
this.mempoolBlockState = {
block: change.block,
transactions: acc.transactions
};
return this.mempoolBlockState;
}
}, {}));
}, {}),
share()
);
this.liveMempoolBlockTransactions$.subscribe();
// Emits the full list of pending accelerations each time it changes
this.liveAccelerations$ = this.accelerations$.pipe(
+19 -5
View File
@@ -35,6 +35,7 @@ export class WebsocketService {
private isTrackingAddresses: string[] | false = false;
private isTrackingAccelerations: boolean = false;
private trackingMempoolBlock: number;
private stoppingTrackMempoolBlock: any | null = null;
private latestGitCommit = '';
private onlineCheckTimeout: number;
private onlineCheckTimeoutTwo: number;
@@ -203,19 +204,31 @@ export class WebsocketService {
this.websocketSubject.next({ 'track-asset': 'stop' });
}
startTrackMempoolBlock(block: number, force: boolean = false) {
startTrackMempoolBlock(block: number, force: boolean = false): boolean {
if (this.stoppingTrackMempoolBlock) {
clearTimeout(this.stoppingTrackMempoolBlock);
}
// skip duplicate tracking requests
if (force || this.trackingMempoolBlock !== block) {
this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true;
this.trackingMempoolBlock = block;
return true;
}
return false;
}
stopTrackMempoolBlock() {
this.websocketSubject.next({ 'track-mempool-block': -1 });
stopTrackMempoolBlock(): void {
if (this.stoppingTrackMempoolBlock) {
clearTimeout(this.stoppingTrackMempoolBlock);
}
this.isTrackingMempoolBlock = false;
this.trackingMempoolBlock = null;
this.stoppingTrackMempoolBlock = setTimeout(() => {
this.stoppingTrackMempoolBlock = null;
this.websocketSubject.next({ 'track-mempool-block': -1 });
this.trackingMempoolBlock = null;
this.stateService.mempoolBlockState = null;
}, 2000);
}
startTrackRbf(mode: 'all' | 'fullRbf') {
@@ -424,6 +437,7 @@ export class WebsocketService {
if (response['projected-block-transactions'].blockTransactions) {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
this.stateService.mempoolBlockUpdate$.next({
block: this.trackingMempoolBlock,
transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
});
} else if (response['projected-block-transactions'].delta) {
@@ -432,7 +446,7 @@ export class WebsocketService {
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
} else {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta));
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta));
}
}
}
+11
View File
@@ -17,6 +17,7 @@ export type AddressType = 'fee'
| 'v0_p2wsh'
| 'v1_p2tr'
| 'confidential'
| 'anchor'
| 'unknown'
const ADDRESS_PREFIXES = {
@@ -188,6 +189,12 @@ export class AddressTypeInfo {
const v = vin[0];
this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm));
}
} else if (this.type === 'unknown') {
for (const v of vin) {
if (v.prevout?.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
}
}
// and there's nothing more to learn from processing inputs for other types
}
@@ -197,6 +204,10 @@ export class AddressTypeInfo {
if (!this.scripts.size) {
this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm));
}
} else if (this.type === 'unknown') {
if (output.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
}
}
+24 -1
View File
@@ -170,8 +170,9 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped {
};
}
export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): MempoolBlockDelta {
export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCompressed): MempoolBlockDelta {
return {
block,
added: delta.added.map(uncompressTx),
removed: delta.removed,
changed: delta.changed.map(tx => ({
@@ -239,3 +240,25 @@ export function md5(inputString): string {
}
return rh(a)+rh(b)+rh(c)+rh(d);
}
export function colorFromRetarget(da: number): string {
const minDA = 0.95;
const maxDA = 1.05;
const midDA = 1;
const red = { r: 220, g: 53, b: 69 };
const grey = { r: 108, g: 117, b: 125 };
const green = { r: 59, g: 204, b: 73 };
const interpolateColor = (color1, color2, ratio) => {
ratio = Math.min(1, Math.max(0, ratio));
const r = Math.round(color1.r + ratio * (color2.r - color1.r));
const g = Math.round(color1.g + ratio * (color2.g - color1.g));
const b = Math.round(color1.b + ratio * (color2.b - color1.b));
return `rgba(${r}, ${g}, ${b}, 0.7)`;
}
return da <= midDA ?
interpolateColor(red, grey, (da - minDA) / (midDA - minDA)) :
interpolateColor(grey, green, (da - midDA) / (maxDA - midDA));
}
@@ -20,6 +20,9 @@
@case ('multisig') {
<span i18n="address.bare-multisig">bare multisig</span>
}
@case ('anchor') {
<span>anchor</span>
}
@case (null) {
<span>unknown</span>
}
@@ -79,7 +79,7 @@
<p class="category" i18n="footer.networks">Networks</p>
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== '') && (currentNetwork !== 'mainnet')"><a [href]="networkLink('mainnet')" i18n="footer.mainnet-explorer">Mainnet Explorer</a></p>
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet') && env.TESTNET_ENABLED"><a [href]="networkLink('testnet')" i18n="footer.testnet3-explorer">Testnet3 Explorer</a></p>
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet4') && env.TESTNET4_ENABLED"><a [href]="networkLink('testnet4')" i18n="footer.testnet4-explorer">Testnet4 Explorer</a> <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></p>
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet4') && env.TESTNET4_ENABLED"><a [href]="networkLink('testnet4')" i18n="footer.testnet4-explorer">Testnet4 Explorer</a></p>
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'signet') && env.SIGNET_ENABLED"><a [href]="networkLink('signet')" i18n="footer.signet-explorer">Signet Explorer</a></p>
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquidtestnet')"><a [href]="networkLink('liquidtestnet')" i18n="footer.liquid-testnet-explorer">Liquid Testnet Explorer</a></p>
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquid')"><a [href]="networkLink('liquid')" i18n="footer.liquid-explorer">Liquid Explorer</a></p>
@@ -5,9 +5,6 @@
@if (stateService.network === 'testnet') {
<span i18n="testnet3-deprecated">Testnet3 is deprecated, and will soon be replaced by <a href="/testnet4">Testnet4</a></span>
}
@if (stateService.network === 'testnet4') {
<span i18n="testnet4-not-finalized">Testnet4 is not yet finalized, and may be reset at anytime.</span>
}
</div>
<button type="button" class="close" (click)="dismissWarning()">
<span aria-hidden="true">&times;</span>
@@ -1,6 +1,6 @@
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
<ng-container *ngIf="link">
<a [routerLink]="link" class="truncate-link" [target]="external ? '_blank' : ''">
<a [routerLink]="link" [queryParams]="queryParams" class="truncate-link" [target]="external ? '_blank' : '_self'">
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
</a>
</ng-container>
@@ -10,6 +10,7 @@ export class TruncateComponent {
@Input() text: string;
@Input() link: any = null;
@Input() external: boolean = false;
@Input() queryParams: any = undefined;
@Input() lastChars: number = 4;
@Input() maxWidth: number = null;
@Input() inline: boolean = false;
+3 -2
View File
@@ -166,6 +166,7 @@ export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate
ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }),
ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }),
multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }),
anchor: () => ({ type: 'anchor', label: 'anchor' }),
};
export class ScriptInfo {
@@ -266,7 +267,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
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);
@@ -286,7 +287,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
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);
+3
View File
@@ -100,6 +100,7 @@ import { MempoolErrorComponent } from './components/mempool-error/mempool-error.
import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component';
import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component';
import { AccelerationSparklesComponent } from '../components/acceleration/sparkles/acceleration-sparkles.component';
import { BlockViewComponent } from '../components/block-view/block-view.component';
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
@@ -225,6 +226,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AccelerationsListComponent,
AccelerationStatsComponent,
PendingStatsComponent,
AccelerationSparklesComponent,
HttpErrorComponent,
TwitterWidgetComponent,
FaucetComponent,
@@ -355,6 +357,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AccelerationsListComponent,
AccelerationStatsComponent,
PendingStatsComponent,
AccelerationSparklesComponent,
HttpErrorComponent,
TwitterWidgetComponent,
TwitterLogin,
+135 -6
View File
@@ -1,10 +1,10 @@
import { TransactionFlags } from './filters.utils';
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils';
import { Transaction } from '../interfaces/electrs.interface';
import { CpfpInfo, RbfInfo } from '../interfaces/node-api.interface';
import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface';
import { StateService } from '../services/state.service';
// 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);
@@ -89,10 +89,13 @@ export function isDERSig(w: string): boolean {
*
* 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.
*/
export function isNonStandard(tx: Transaction): boolean {
export function isNonStandard(tx: Transaction, height?: number, network?: string): boolean {
// version
if (tx.version > TX_MAX_STANDARD_VERSION) {
if (isNonStandardVersion(tx, height, network)) {
return true;
}
@@ -139,6 +142,8 @@ export function isNonStandard(tx: Transaction): boolean {
}
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true;
} else if (isNonStandardAnchor(tx, height, network)) {
return true;
}
// TODO: bad-witness-nonstandard
}
@@ -203,6 +208,51 @@ export function isNonStandard(tx: Transaction): boolean {
return false;
}
// Individual versioned standardness rules
const V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
function isNonStandardVersion(tx: Transaction, height?: number, network?: string): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& network != null
&& V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height <= V3_STANDARDNESS_ACTIVATION_HEIGHT[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;
}
const ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
function isNonStandardAnchor(tx: Transaction, height?: number, network?: string): boolean {
if (
height != null
&& network != null
&& ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height <= ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[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;
}
// 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
@@ -289,7 +339,7 @@ export function isBurnKey(pubkey: string): boolean {
].includes(pubkey);
}
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean): bigint {
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean, height?: number, network?: string): bigint {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
@@ -439,7 +489,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
flags |= TransactionFlags.batch_payout;
}
if (isNonStandard(tx)) {
if (isNonStandard(tx, height, network)) {
flags |= TransactionFlags.nonstandard;
}
@@ -458,4 +508,83 @@ export function getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean):
} else {
return tx.effectiveFeePerVsize;
}
}
export function identifyPrioritizedTransactions(transactions: TransactionStripped[]): { 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(); // 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: TransactionStripped[] = 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 = 0;
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 };
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more