Compare commits

...
Author SHA1 Message Date
Stephan Oeste 8bde2d519a save mainnet mempool every hour to disk 2024-08-14 20:11:08 +02: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
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
natsoni 132d6204c3 Fix pool pie position on safari 2024-08-08 11:23:56 +02:00
wiz 77c6ad5576 Merge pull request #5438 from mempool/natsoni/hide-fee-delta-on-confirmed
Hide fee delta on accelerated tx if bid boost is 0
2024-08-07 19:38:04 -04:00
wiz 4d35845c18 Merge pull request #5441 from mempool/simon/remove-testnet4-beta
remove testnet4 beta tag
2024-08-07 19:36:55 -04:00
wiz 3d8a4a85f7 Merge pull request #5433 from mempool/simon/remove-mempool-goggles-beta
remove mempool googles [beta] tag
2024-08-07 19:36:45 -04:00
wiz 1545347a45 Merge pull request #5443 from mempool/simon/fix-broken-sponsor-image-proxy
fix broken sponsor image proxy
2024-08-07 18:11:59 -04:00
softsimon 9facf28ba5 pull from transifex 2024-08-07 23:57:11 +02:00
softsimon d6eb98561b Merge pull request #5439 from mempool/natsoni/purple-acc-fee-rate
Consistent purple accelerated fee rate
2024-08-07 23:42:37 +02:00
softsimon 0f688e8347 fix broken sponsor image proxy 2024-08-07 23:41:53 +02:00
wiz 7f53741a7b Merge pull request #5442 from mempool/simon/timesincepaid-sorry-time-increase
increasing time since paid sorry message
2024-08-07 17:19:11 -04:00
softsimon d5672691e1 increasing time since paid sorry message 2024-08-07 22:06:22 +02:00
softsimon e6049c707b remove testnet4 beta tag 2024-08-07 22:03:47 +02:00
natsoni 91e74e769c Purple accelerated fee rate everywhere 2024-08-07 14:19:59 +02:00
natsoni 7f252f06b7 Hide fee delta on accelerated tx with bidBoost=0 2024-08-07 11:11:31 +02:00
softsimon 1b9d3f669d remove mempool googles [beta] tag 2024-08-07 00:30:20 +02:00
wiz ef0ba9a77a ops: Add mempool-update-repo script 2024-08-06 18:30:11 -04:00
wiz 15f10736e2 Merge pull request #5432 from mempool/simon/match-any-onion
match any onion
2024-08-06 18:26:31 -04:00
softsimon 924399df46 match any onion 2024-08-07 00:24:23 +02:00
wiz cddff129b3 Merge pull request #5431 from mempool/simon/fix-services-onion
fix services onion url
2024-08-06 17:29:15 -04:00
softsimon 7c90e8ae06 fix services onion url 2024-08-06 23:27:59 +02:00
wiz 34d996c7cb Merge pull request #5430 from mempool/natsoni/dynamically-show-oob-fee
Immediately show oob fee on accelerated transaction
2024-08-06 15:00:49 -04:00
wiz 641a2ae3ae Remove testnet4 not-yet-finalized warning now that BIP is merged 2024-08-05 15:37:29 -04:00
natsoni 11a849ef28 Immediately show oob fee on accelerated transaction 2024-08-05 21:01:33 +02:00
wiz 7b0347e846 Bump version string to 3.0.0-beta 2024-08-05 14:55:59 -04:00
wiz bb1352ed58 Merge pull request #5427 from mempool/mononaut/fix-other-liquid-migration
fix db migration 75 on liquid
2024-08-05 14:09:36 -04:00
wiz fa6456b92c Merge pull request #5422 from mempool/natsoni/purple-accelerated-rate
Purple accelerated fee rate
2024-08-05 14:09:14 -04:00
wiz bfea19238b Merge pull request #5420 from mempool/natsoni/add-tooltip-acc-fee
Add tooltip to acceleration fee
2024-08-05 14:04:42 -04:00
wiz 36b91cfdfd Merge pull request #5421 from mempool/mononaut/rbf-tracker-redirect
Fix broken pizza rbf link to /tracker
2024-08-05 14:04:20 -04:00
wiz 7b56212064 Merge pull request #5425 from mempool/natsoni/clear-mining-cache-network-change
Clear mining service cache on network change
2024-08-05 14:03:14 -04:00
wiz d4be3c2c4c Merge pull request #5428 from mempool/mononaut/payment-method-click
[accelerator] fix click binding on payment method buttons
2024-08-05 14:02:51 -04:00
wiz 3a9f06f651 Merge pull request #5429 from mempool/mononaut/fix-block-unfurl-loading
Fix stray loading spinner in block unfurl
2024-08-05 14:02:34 -04:00
Mononaut e652eb339d Fix stray loading spinner in block unfurl 2024-08-05 17:08:44 +00:00
softsimon 96435c329f Merge pull request #5426 from mempool/mononaut/fix-liquid-migration
fix liquid db migration
2024-08-05 18:47:26 +02:00
Mononaut 1c69613d65 [accelerator] fix click binding on payment method buttons 2024-08-05 16:15:53 +00:00
Mononaut 3707763e30 fix db migration 75 on liquid 2024-08-05 16:03:22 +00:00
Mononaut b6ce8229f0 fix liquid db migration 2024-08-05 15:49:16 +00:00
natsoni b62ae9b6f6 Clear mining service cache on network change 2024-08-05 15:45:39 +02:00
Mononaut f61ace2f92 Fix broken pizza rbf link to /tracker 2024-08-05 10:31:01 +00:00
natsoni 2b572f2494 Purple accelerated fee rate 2024-08-05 12:20:49 +02:00
natsoni c0e4c1efe1 Fix text wrap 2024-08-05 11:53:01 +02:00
natsoni 8078caaa89 Add tooltip to acceleration fee 2024-08-05 11:36:35 +02:00
wiz 5eb117165f Revert "ops: Remove potentially dangerous env var in rust build process"
This reverts commit a2dcf0d545.
2024-08-04 21:19:44 -04:00
wiz a2dcf0d545 ops: Remove potentially dangerous env var in rust build process 2024-08-04 21:06:36 -04:00
wiz 212d58f917 Change accelerate checkout redirect from /tracker/ to /tx/ 2024-08-04 20:52:49 -04:00
wiz d1eec80afb Delete redirect to /tracker/ for cash.app 2024-08-04 20:45:05 -04:00
wiz 05c6709926 Merge pull request #5417 from mempool/simon/hide-fiat-buttons-correctly
hide fiat buttons correctly
2024-08-04 19:45:07 -04:00
softsimon f1a48db9ee hide fiat buttons correctly 2024-08-05 01:43:58 +02:00
wiz 76ce43d289 Merge pull request #5416 from mempool/simon/enable-acc-button-enterprise
enable accelerator button on enterprise instances
2024-08-04 19:22:33 -04:00
softsimon 51f5b728f3 enable accelerator button on enterprise instances 2024-08-05 01:20:40 +02:00
wiz 8fa1863aff Merge pull request #5415 from mempool/simon/fix-invalid-json-response-requestAcceleration
fix invalid json response from requestAcceleration
2024-08-04 19:12:06 -04:00
softsimon e3d1d9c1c0 fix invalid json response from requestAcceleration 2024-08-05 01:11:18 +02:00
wiz f2f8d91e10 Merge pull request #5410 from mempool/natsoni/show-oob-fee
Show oob fees on tx details
2024-08-04 19:02:20 -04:00
wiz 11ef090846 Merge pull request #5414 from mempool/simon/only-enable-fiat-prod-staging
only enable fiat on prod and staging
2024-08-04 19:01:54 -04:00
softsimon 5cacd2635e only enable fiat on prod, staging and dev 2024-08-05 01:00:58 +02:00
softsimon 1b4780c25b Merge pull request #5409 from mempool/natsoni/fix-truncated-link
Fix truncated link to not refresh full window
2024-08-05 00:16:15 +02:00
softsimon 5ea44f2e7d Merge pull request #5408 from mempool/natsoni/pizza-tracker-fix-crash
Pizza tracker: handle transaction not yet in mempool
2024-08-04 23:39:39 +02:00
softsimon 261c794817 pull from transifex 2024-08-04 23:36:03 +02:00
natsoni de9fae5cd7 Show oob fees on tx details 2024-08-04 17:07:38 +02:00
natsoni 439c52af30 Fix truncated link to not refresh full window 2024-08-04 14:36:42 +02:00
softsimon 4dbcd6ca18 pull from transifex 2024-08-04 10:18:18 +02:00
softsimon 2921c94520 Merge pull request #5354 from mempool/mononaut/v1-audits
v1 audits
2024-08-03 21:55:19 +02:00
softsimon ab4a258be3 Merge pull request #5406 from mempool/simon/fix-mobile-rbf-test
fix mobile rbf test
2024-08-03 19:07:49 +02:00
softsimon 21b0d50947 fix mobile rbf test 2024-08-03 18:58:55 +02:00
softsimon 233ec112c2 Merge pull request #5405 from TechMiX/fix/more-accel-rtl-issues
fix: acceleration rtl layout issues
2024-08-03 18:24:22 +02:00
TechMiX 6f0a5c9b44 fix: accelerationn rtl layout issues 2024-08-03 14:39:50 +02:00
softsimon 2bc243b115 accel list i18n fixes 2024-08-03 00:26:37 +02:00
softsimon 154f8e65a7 accel status i18n fix 2024-08-03 00:20:38 +02:00
wiz cc9855aa65 Merge pull request #5404 from mempool/nymkappa/square-loading
[square] use mirror to serve square.js and load it sooner
2024-08-02 17:52:44 -04:00
nymkappa 7b45d922bc [square] i'm an idiot 2024-08-02 23:35:30 +02:00
nymkappa 9488ca50a3 [square] use mirror to serve square.js and load it sooner 2024-08-02 23:27:40 +02:00
wiz 9f559248cc Merge pull request #5403 from mempool/nymkappa/square-loading
[square] retry web sdk faster
2024-08-02 17:24:00 -04:00
nymkappa 067aac4d06 [square] retry web sdk faster 2024-08-02 23:04:15 +02:00
wiz 37eb17cc22 Merge pull request #5402 from mempool/nymkappa/square-loading
[square] fix web sdk retry logic
2024-08-02 16:50:41 -04:00
nymkappa c7382a1c6c [square] fix web sdk retry logic 2024-08-02 22:48:04 +02:00
softsimon f782693b26 Merge pull request #5401 from mempool/mononaut/acc-pie-colors
adjust pie colors to handle more pools
2024-08-02 21:40:36 +02:00
wiz 908988d06e Merge pull request #5398 from mempool/natsoni/fix-accelerator-loader
Fix accelerator skeleton loader
2024-08-02 14:41:29 -04:00
wiz a0c21e4120 Merge pull request #5395 from mempool/nymkappa/accel-status
[accelerator] fix accel status in widget
2024-08-02 14:25:54 -04:00
wiz bd96cbd701 Merge pull request #5399 from mempool/natsoni/fix-mobile-tx-routing
Remove query param redirect on tx page
2024-08-02 14:23:54 -04:00
wiz 9cccdcf70f Merge pull request #5400 from mempool/natsoni/pizza-tracker-loader
Align pizza tracker skeleton loader
2024-08-02 14:23:21 -04:00
natsoni 2489fc4902 Fix need to double click on previous to go back 2024-08-02 19:00:52 +02:00
natsoni e378df4158 Fix pizza tracker crash on unseen transaction 2024-08-02 18:52:07 +02:00
Mononaut fa5f758875 adjust pie colors to handle more pools 2024-08-02 16:44:36 +00:00
natsoni a46f15e7ec Align pizza tracker skeleton loader 2024-08-02 17:37:53 +02:00
natsoni 46ecd2a51f Fix accelerator skeleton loader 2024-08-02 16:53:34 +02:00
softsimon c1aaf2f61a pull from transifex 2024-08-02 14:04:49 +02:00
Mononaut d2b57c8d4f added/prioritized dual audit status 2024-08-02 10:46:07 +00:00
softsimon f27a5700fa pull from transifex 2024-08-02 02:43:34 +02:00
nymkappa 17522cca6e [accelerator] ⌛ 2024-08-02 00:24:50 +02:00
nymkappa 3f2581886d [accelerator] fix accel status in widget 2024-08-02 00:20:34 +02:00
wiz c5beee3a40 ops: Fix nginx routing for /api/v1/accelerations 2024-08-01 17:04:00 -04:00
wiz 94e223e8da Merge pull request #5010 from mempool/mononaut/tracker-tx-routing
Transparently redirect direct mobile tx visits to pizza tracker
2024-08-01 14:30:56 -04:00
wiz 416e1bb4cb Merge pull request #5393 from mempool/mononaut/accelerate-anchor
[accelerator] add anchor element
2024-08-01 14:19:22 -04:00
wiz 564dc08509 Merge pull request #5392 from mempool/orangesurf/privacy-policy
Update PP
2024-08-01 14:19:01 -04:00
Mononaut b9334c93f5 Handle accelerations in v1 audit 2024-08-01 14:07:19 +00:00
Mononaut 67761230e3 frontend support for v1 block audits 2024-08-01 14:07:19 +00:00
Mononaut 7cc01af631 Implement v1 block audits 2024-08-01 14:07:19 +00:00
Mononaut 96e2e6060b Migrate audits from v0 to v1 2024-08-01 14:07:19 +00:00
Mononaut 0723778e7c Update audit schema version 2024-08-01 14:07:19 +00:00
orangesurf 39d7e4ac3e Merge branch 'master' into orangesurf/privacy-policy 2024-08-01 13:44:34 +02:00
softsimon 1869368a49 pull from transifex 2024-08-01 10:50:01 +02:00
orangesurf b83f19f186 Merge branch 'master' into orangesurf/privacy-policy 2024-07-31 23:50:58 +02:00
Mononaut b248aad6d9 [accelerator] add anchor element 2024-07-31 17:08:39 +00:00
softsimon 87c9f920c1 Merge pull request #5391 from mempool/natsoni/add-opacity-pools-logo
Set pool logos opacity to 0.3
2024-07-31 18:34:09 +02:00
orangesurf ea0e339d60 Update PP 2024-07-31 16:02:26 +02:00
orangesurf 0a0b5e52fe Update PP 2024-07-31 13:55:21 +02:00
natsoni 5314f35974 Make logos 0.3 opacity 2024-07-31 12:22:49 +02:00
softsimon 87d2f6cf90 Merge pull request #5389 from TechMiX/fix/acc-rtl-issues
fix: acceleration rtl issues
2024-07-31 11:16:29 +02:00
softsimon 4266bdcbb6 pull from transifex 2024-07-31 09:29:55 +02:00
softsimon 73e68534f1 Merge pull request #5390 from mempool/dependabot/npm_and_yarn/backend/babel/core-7.25.2
Bump @babel/core from 7.24.0 to 7.25.2 in /backend
2024-07-31 02:56:37 -04:00
dependabot[bot] ee5b383a46 Bump @babel/core from 7.24.0 to 7.25.2 in /backend
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.24.0 to 7.25.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.2/packages/babel-core)

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-29 02:07:00 +00:00
softsimon 558b876d47 pull from transifex 2024-07-28 19:48:15 -05:00
TechMiX fb63af5070 fix various rtl issues 2024-07-29 02:17:09 +02:00
wiz d02a2ccf65 Merge pull request #5380 from mempool/simon/1w-1m
fixes crash with missing pool partner
2024-07-28 14:40:50 -05:00
softsimon e3bb812203 fixes crash with missing pool partner
fixes #5379
2024-07-28 14:10:50 -05:00
softsimon e3af4ea61c pull from transifex 2024-07-28 13:20:58 -05:00
softsimon 7d10b32861 pull from transifex 2024-07-26 16:43:17 -05:00
wiz adea897e93 Merge branch 'master' into mononaut/tracker-tx-routing 2024-07-26 11:29:31 -05:00
wiz 17ec50dba0 Merge pull request #5371 from mempool/natsoni/timeline-tooltip
Add tooltip to acceleration timeline
2024-07-26 11:12:15 -05:00
Mononaut 53a36d042f Fix /tx redirect merge conflicts 2024-07-26 16:11:59 +00:00
softsimon 7531a53b2e correct i18n 2024-07-26 11:06:36 -05:00
Mononaut d59bc085e5 Redirect direct mobile tx visits to pizza tracker 2024-07-26 15:54:22 +00:00
wiz 5d37e08c64 Merge branch 'master' into natsoni/timeline-tooltip 2024-07-26 10:44:01 -05:00
wiz b5d89b83fa Merge pull request #5368 from mempool/nymkappa/google-pay
[accelerator] add support for Google Pay payment
2024-07-26 10:43:50 -05:00
nymkappa 1245673575 [accelerator] polish UI 2024-07-26 17:39:58 +02:00
softsimon 62a72755c7 pull from transifex 2024-07-26 10:25:29 -05:00
wiz 5b8ec8925a Merge pull request #5373 from mempool/mononaut/no-405
[accelerator] remove dumb log request 405 response
2024-07-26 10:22:38 -05:00
softsimon 262866c15b Merge pull request #5369 from mempool/nymkappa/getjwtemptynonofficial
[services] getJWT call returns nothing if non official
2024-07-26 06:36:21 -05:00
softsimon aec7eb57c2 Merge pull request #5370 from mempool/natsoni/fix-loading-spinner
Fix loading spinner z-index
2024-07-26 05:10:25 -05:00
Mononaut 8d3b4733f5 [accelerator] remove dumb 405 log request response 2024-07-26 09:49:42 +00:00
nymkappa 3e4debdf7a Merge branch 'master' into nymkappa/google-pay 2024-07-26 11:38:47 +02:00
nymkappa b719b76999 Merge pull request #5372 from mempool/mononaut/google-play-minor-fixes
minor accelerator checkout fixes
2024-07-26 11:37:54 +02:00
Mononaut 6adbda5185 Fix accelerator checkout linting & type errors 2024-07-26 09:27:18 +00:00
natsoni 01311d0ba1 Add tooltip to timeline 2024-07-26 11:21:24 +02:00
Mononaut 6081daacef [accelerator] add missing getters etc 2024-07-26 09:06:47 +00:00
natsoni 54c9970386 Merge branch 'master' into natsoni/timeline-tooltip 2024-07-26 11:04:17 +02:00
softsimon 1e4a599055 fix accelerations row height 2024-07-25 21:05:32 -05:00
softsimon b051861d61 Merge pull request #5365 from mempool/natsoni/fix-mining-data
Get all pools in accelerations list
2024-07-25 21:03:52 -05:00
natsoni 3c7deafffd Fix loading spinner z-index 2024-07-26 00:00:14 +02:00
nymkappa 845123fc63 [services] getJWT call returns nothing if non official 2024-07-25 23:04:53 +02:00
nymkappa d3e3650cac [accelerator] avoid premature square setup call 2024-07-25 21:49:48 +02:00
softsimon 008cc385da pull from transifex 2024-07-25 13:01:37 -05:00
softsimon 817a6bef6e Merge pull request #5367 from mempool/knorrium/change_default_var
Set the default value to the SERVICES_API variable
2024-07-25 12:59:40 -05:00
Felipe Knorr Kuhn 8dd8fe5fb1 Fix string value 2024-07-25 12:57:36 -05:00
softsimon c734a81f08 pull from transifex 2024-07-25 12:27:32 -05:00
Felipe Knorr Kuhn a0f4c260ab Set the default URL for the backend 2024-07-25 12:20:01 -05:00
Felipe Knorr Kuhn 000a989055 Set the default value to the SERVICES_API variable 2024-07-25 11:29:17 -05:00
natsoni 90331e2c1b Get all pools in accelerations list 2024-07-25 17:45:08 +02:00
wiz 78ac0137b3 Merge pull request #5350 from mempool/orangesurf/2024-07-19
Update webserver line
2024-07-25 10:21:53 -05:00
natsoni 3d9133c47e Add fee delta to acceleration data 2024-07-25 16:52:10 +02:00
nymkappa 481859bc8f [accelerator] add support for Google Pay payment 2024-07-25 15:54:24 +02:00
orangesurf 811feec145 Merge branch 'master' into orangesurf/2024-07-19 2024-07-25 18:47:27 +09:00
softsimon b3e59c06e9 add new accelerator button config to sample 2024-07-25 04:07:18 -05:00
softsimon 67d44e3d6f Merge pull request #5359 from mempool/natsoni/fix-loop-calling-transactionTimes
Prevent never ending loop of calls to transactionTimes
2024-07-25 03:38:06 -05:00
softsimon ca4b1943a8 moving code block 2024-07-25 03:35:17 -05:00
natsoni df0f244bd1 Prevent never ending loop of calls to transactionTimes 2024-07-25 00:53:13 +02:00
softsimon fe1ad86885 Merge pull request #5362 from mempool/natsoni/more-data-acc-table
Add fee delta and pool name to acceleration list
2024-07-24 17:42:28 -05:00
natsoni aee2454a98 Add pool name to acceleration list 2024-07-25 00:24:08 +02:00
softsimon 5c814d9c22 pending balance -> pending 2024-07-24 17:21:55 -05:00
wiz 2c5d7fbc9f Merge pull request #5363 from mempool/nymkappa/apple-pay-hotfix
[accelerator] hide fiat payment method section if none available
2024-07-24 17:11:01 -05:00
nymkappa 570f7841ce [accelerator] hide fiat payment method section if none available 2024-07-25 00:10:27 +02:00
wiz 117c066425 Merge pull request #5353 from mempool/nymkappa/apple-pay
[accelerator] add support for acceleration with apple pay
2024-07-24 17:01:23 -05:00
softsimon 96f9f66e7f changing unconfirmed to pending balance/utxo 2024-07-24 16:56:30 -05:00
wiz ce2742ff9c Merge branch 'master' into nymkappa/apple-pay 2024-07-24 16:47:57 -05:00
nymkappa 4d44ee55fc [accelerator] add missing getters for applepay 2024-07-24 22:20:52 +02:00
nymkappa 29875e0095 Merge branch 'master' into nymkappa/apple-pay 2024-07-24 22:04:44 +02:00
orangesurf 301f1821ae Merge branch 'master' into orangesurf/2024-07-19 2024-07-23 20:48:03 +09:00
orangesurf 82360f5525 Merge branch 'master' into orangesurf/2024-07-19 2024-07-22 17:08:47 +09:00
nymkappa 8762ccaa09 [accelerator] remove attempt to align fiat payment methods 2024-07-21 23:22:11 +02:00
nymkappa 3f7a24fb52 [accelerator] only show apple pay if available 2024-07-21 23:08:08 +02:00
nymkappa 09b09710e4 [accelerator] fix cashapp acceleration on mobile 2024-07-21 23:07:55 +02:00
nymkappa 08d3beed72 [accelerator] on mobile, autoscroll after selection cashapp or applepay 2024-07-21 22:38:49 +02:00
nymkappa 920f225e6c [accelerator] add support for acceleration with apple pay 2024-07-21 22:17:47 +02:00
orangesurf 84ba721407 Update webserver line 2024-07-19 11:48:47 +02:00
156 changed files with 17431 additions and 9885 deletions

No files matched your search

+332 -402
View File
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "mempool-backend", "name": "mempool-backend",
"version": "3.0.0-dev", "version": "3.0.0-beta",
"description": "Bitcoin mempool visualizer and blockchain explorer backend", "description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space", "homepage": "https://mempool.space",
@@ -39,7 +39,7 @@
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.24.0", "@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "~1.7.2", "axios": "~1.7.2",
@@ -47,16 +47,16 @@
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.19.2", "express": "~4.19.2",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.10.0", "mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6", "redis": "^4.7.0",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3", "typescript": "~4.9.3",
"ws": "~8.18.0" "ws": "~8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
"@babel/core": "^7.24.0", "@babel/core": "^7.25.2",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
+1 -1
View File
@@ -70,7 +70,7 @@ class AboutRoutes {
res.status(500).end(); 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/', '')}`; const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try { try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
+34 -7
View File
@@ -6,20 +6,22 @@ import rbfCache from './rbf-cache';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
class Audit { class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false) auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { : { 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) { 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 matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template const added: string[] = []; // present in mined block, not in template
const 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
const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN 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 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 accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse const isCensored = {}; // missing, without excuse
const isDisplaced = {}; const isDisplaced = {};
const isAccelerated = {};
let displacedWeight = 0; let displacedWeight = 0;
let matchedWeight = 0; let matchedWeight = 0;
let projectedWeight = 0; let projectedWeight = 0;
@@ -32,6 +34,7 @@ class Audit {
inBlock[tx.txid] = tx; inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) { if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid); accelerated.push(tx.txid);
isAccelerated[tx.txid] = true;
} }
} }
// coinbase is always expected // coinbase is always expected
@@ -113,11 +116,16 @@ class Audit {
} else { } else {
if (rbfCache.has(tx.txid)) { if (rbfCache.has(tx.txid)) {
rbf.push(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]) { if (mempool[tx.txid]) {
prioritized.push(tx.txid); if (isDisplaced[tx.txid]) {
added.push(tx.txid);
}
} else { } else {
added.push(tx.txid); unseen.push(tx.txid);
} }
} }
overflowWeight += tx.weight; overflowWeight += tx.weight;
@@ -125,6 +133,24 @@ class Audit {
totalWeight += tx.weight; totalWeight += tx.weight;
} }
// identify "prioritized" transactions
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 = transactions.length - 1; i > 0; i--) {
const blockTx = 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.effectiveFeePerVsize || 0) < lastEffectiveRate) {
prioritized.push(blockTx.txid);
// accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference
} else if (!isAccelerated[blockTx.txid]) {
lastEffectiveRate = blockTx.effectiveFeePerVsize || 0;
}
}
// transactions missing from near the end of our template are probably not being censored // transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight); let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
let maxOverflowRate = 0; let maxOverflowRate = 0;
@@ -165,6 +191,7 @@ class Audit {
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return { return {
unseen,
censored: Object.keys(isCensored), censored: Object.keys(isCensored),
added, added,
prioritized, prioritized,
@@ -165,6 +165,7 @@ class BitcoinRoutes {
acceleration: tx.acceleration, acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined, acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined, acceleratedAt: tx.acceleratedAt || undefined,
feeDelta: tx.feeDelta || undefined,
}); });
return; return;
} }
+16 -10
View File
@@ -33,6 +33,7 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool'; import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository'; import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
@@ -439,7 +440,7 @@ class Blocks {
if (config.MEMPOOL.BACKEND === 'esplora') { 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); const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
if (cpfpSummary) { if (cpfpSummary) {
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
@@ -904,7 +905,12 @@ 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 blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
@@ -927,12 +933,12 @@ class Blocks {
const newBlock = await this.$indexBlock(lastBlock.height - i); const newBlock = await this.$indexBlock(lastBlock.height - i);
this.blocks.push(newBlock); this.blocks.push(newBlock);
this.updateTimerProgress(timer, `reindexed block`); this.updateTimerProgress(timer, `reindexed block`);
let cpfpSummary; let newCpfpSummary;
if (config.MEMPOOL.CPFP_INDEXING) { 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`); 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`); this.updateTimerProgress(timer, `reindexed block summary`);
} }
await mining.$indexDifficultyAdjustments(); await mining.$indexDifficultyAdjustments();
@@ -981,7 +987,7 @@ class Blocks {
// start async callbacks // start async callbacks
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`); 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 (block.height % 2016 === 0) {
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
@@ -1178,7 +1184,7 @@ class Blocks {
}; };
}), }),
}; };
summaryVersion = 1; summaryVersion = cpfpSummary.version;
} else { } else {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
@@ -1397,11 +1403,11 @@ class Blocks {
return this.currentBlockHeight; 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; let transactions = txs;
if (!transactions) { if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') { 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) { if (!transactions) {
const block = await bitcoinClient.getBlock(hash, 2); const block = await bitcoinClient.getBlock(hash, 2);
@@ -1413,7 +1419,7 @@ class Blocks {
} }
if (transactions?.length != null) { if (transactions?.length != null) {
const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]); const summary = calculateFastBlockCpfp(height, transactions);
await this.$saveCpfp(hash, height, summary); await this.$saveCpfp(hash, height, summary);
+1 -1
View File
@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import { Request } from 'express'; 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 config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net'; import { isIP } from 'net';
+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 CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
const MAX_CLUSTER_ITERATIONS = 100; 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 clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
@@ -93,6 +93,7 @@ export function calculateFastBlockCpfp(height: number, transactions: Transaction
return { return {
transactions, transactions,
clusters, clusters,
version: 1,
}; };
} }
@@ -159,6 +160,7 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
return { return {
transactions: transactions.map(tx => txMap[tx.txid]), transactions: transactions.map(tx => txMap[tx.txid]),
clusters: clusterArray, clusters: clusterArray,
version: 2,
}; };
} }
+13 -4
View File
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 80; private static currentVersion = 81;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; 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 `TRY` float DEFAULT "-1"');
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"'); await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
await this.$executeQuery('TRUNCATE hashrates'); if (isBitcoin === true) {
await this.$executeQuery('TRUNCATE difficulty_adjustments'); await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`); await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
}
await this.updateToSchemaVersion(75); await this.updateToSchemaVersion(75);
} }
@@ -691,6 +693,13 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL'); await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
await this.updateToSchemaVersion(80); 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);
}
} }
/** /**
+2
View File
@@ -453,6 +453,7 @@ class MempoolBlocks {
mempoolTx.acceleration = true; mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added; mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) { for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) { if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true; mempool[ancestor.txid].cpfpDirty = true;
@@ -460,6 +461,7 @@ class MempoolBlocks {
mempool[ancestor.txid].acceleration = true; mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
} }
} else { } else {
+1 -1
View File
@@ -337,7 +337,7 @@ export function makeBlockTemplate(candidates: MempoolTransactionExtended[], acce
let failures = 0; let failures = 0;
while (mempoolArray.length || modified.length) { while (mempoolArray.length || modified.length) {
// skip invalid transactions // skip invalid transactions
while (mempoolArray[0].used || mempoolArray[0].modified) { while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
mempoolArray.shift(); mempoolArray.shift();
} }
+1 -5
View File
@@ -465,16 +465,12 @@ class MiningRoutes {
} }
private async $requestAcceleration(req: Request, res: Response): Promise<void> { private async $requestAcceleration(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS || config.MEMPOOL.OFFICIAL) {
res.status(405).send('not available.');
return;
}
res.setHeader('Pragma', 'no-cache'); res.setHeader('Pragma', 'no-cache');
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
res.setHeader('expires', -1); res.setHeader('expires', -1);
try { try {
accelerationApi.accelerationRequested(req.params.txid); accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send('ok'); res.status(200).send();
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }
+5 -2
View File
@@ -37,6 +37,7 @@ export interface AccelerationHistory {
}; };
class AccelerationApi { class AccelerationApi {
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations'); private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
private _accelerations: Acceleration[] | null = null; private _accelerations: Acceleration[] | null = null;
private lastPoll = 0; private lastPoll = 0;
@@ -52,7 +53,9 @@ class AccelerationApi {
} }
public accelerationRequested(txid: string): void { public accelerationRequested(txid: string): void {
this.myAccelerations[txid] = { status: 'requested', added: Date.now() }; if (this.onDemandPollingEnabled) {
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
}
} }
public accelerationConfirmed(): void { public accelerationConfirmed(): void {
@@ -70,7 +73,7 @@ class AccelerationApi {
} }
public async $updateAccelerations(): Promise<Acceleration[] | null> { public async $updateAccelerations(): Promise<Acceleration[] | null> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) { if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations(); const accelerations = await this.$fetchAccelerations();
if (accelerations) { if (accelerations) {
this._accelerations = accelerations; this._accelerations = accelerations;
+11 -3
View File
@@ -3,7 +3,7 @@ import * as WebSocket from 'ws';
import { import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse, BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo, OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids MempoolDelta, MempoolDeltaTxids
} from '../mempool.interfaces'; } from '../mempool.interfaces';
import blocks from './blocks'; import blocks from './blocks';
import memPool from './mempool'; import memPool from './mempool';
@@ -823,6 +823,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined, accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined, acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined, acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
}, },
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid), accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
}; };
@@ -864,6 +865,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined, accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined, acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined, acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
}; };
if (!mempoolTx.cpfpChecked) { if (!mempoolTx.cpfpChecked) {
calculateMempoolTxCpfp(mempoolTx, newMempool); calculateMempoolTxCpfp(mempoolTx, newMempool);
@@ -931,6 +933,8 @@ class WebsocketHandler {
throw new Error('No WebSocket.Server have been set'); throw new Error('No WebSocket.Server have been set');
} }
const blockTransactions = structuredClone(transactions);
this.printLogs(); this.printLogs();
await statistics.runStatistics(); await statistics.runStatistics();
@@ -940,7 +944,7 @@ class WebsocketHandler {
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool); let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
const accelerations = Object.values(mempool.getAccelerations()); 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()); const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions); memPool.handleMinedRbfTransactions(rbfTransactions);
@@ -960,7 +964,7 @@ class WebsocketHandler {
} }
if (Common.indexingEnabled()) { 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 matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : []; const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@@ -982,9 +986,11 @@ class WebsocketHandler {
}); });
BlocksAuditsRepository.$saveAudit({ BlocksAuditsRepository.$saveAudit({
version: 1,
time: block.timestamp, time: block.timestamp,
height: block.height, height: block.height,
hash: block.id, hash: block.id,
unseenTxs: unseen,
addedTxs: added, addedTxs: added,
prioritizedTxs: prioritized, prioritizedTxs: prioritized,
missingTxs: censored, missingTxs: censored,
@@ -1138,6 +1144,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined, accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined, acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined, acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
}, },
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid), accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
}); });
@@ -1160,6 +1167,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined, accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined, acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined, acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
}; };
} }
} }
+2
View File
@@ -10,6 +10,7 @@ import config from './config';
import auditReplicator from './replication/AuditReplication'; import auditReplicator from './replication/AuditReplication';
import statisticsReplicator from './replication/StatisticsReplication'; import statisticsReplicator from './replication/StatisticsReplication';
import AccelerationRepository from './repositories/AccelerationRepository'; import AccelerationRepository from './repositories/AccelerationRepository';
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
export interface CoreIndex { export interface CoreIndex {
name: string; name: string;
@@ -192,6 +193,7 @@ class Indexer {
await auditReplicator.$sync(); await auditReplicator.$sync();
await statisticsReplicator.$sync(); await statisticsReplicator.$sync();
await AccelerationRepository.$indexPastAccelerations(); await AccelerationRepository.$indexPastAccelerations();
await BlocksAuditsRepository.$migrateAuditsV0toV1();
// do not wait for classify blocks to finish // do not wait for classify blocks to finish
blocks.$classifyBlocks(); blocks.$classifyBlocks();
} catch (e) { } catch (e) {
+7 -2
View File
@@ -29,9 +29,11 @@ export interface PoolStats extends PoolInfo {
} }
export interface BlockAudit { export interface BlockAudit {
version: number,
time: number, time: number,
height: number, height: number,
hash: string, hash: string,
unseenTxs: string[],
missingTxs: string[], missingTxs: string[],
freshTxs: string[], freshTxs: string[],
sigopTxs: string[], sigopTxs: string[],
@@ -126,6 +128,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
acceleration?: boolean; acceleration?: boolean;
acceleratedBy?: number[]; acceleratedBy?: number[];
acceleratedAt?: number; acceleratedAt?: number;
feeDelta?: number;
replacement?: boolean; replacement?: boolean;
uid?: number; uid?: number;
flags?: number; flags?: number;
@@ -382,8 +385,9 @@ export interface CpfpCluster {
} }
export interface CpfpSummary { export interface CpfpSummary {
transactions: TransactionExtended[]; transactions: MempoolTransactionExtended[];
clusters: CpfpCluster[]; clusters: CpfpCluster[];
version: number;
} }
export interface Statistic { export interface Statistic {
@@ -449,7 +453,7 @@ export interface OptimizedStatistic {
export interface TxTrackingInfo { export interface TxTrackingInfo {
replacedBy?: string, replacedBy?: string,
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number }, position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
cpfp?: { cpfp?: {
ancestors?: Ancestor[], ancestors?: Ancestor[],
bestDescendant?: Ancestor | null, bestDescendant?: Ancestor | null,
@@ -462,6 +466,7 @@ export interface TxTrackingInfo {
accelerated?: boolean, accelerated?: boolean,
acceleratedBy?: number[], acceleratedBy?: number[],
acceleratedAt?: number, acceleratedAt?: number,
feeDelta?: number,
confirmed?: boolean confirmed?: boolean
} }
+4 -2
View File
@@ -31,11 +31,11 @@ class AuditReplication {
const missingAudits = await this.$getMissingAuditBlocks(); const missingAudits = await this.$getMissingAuditBlocks();
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication'); logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
let totalSynced = 0; let totalSynced = 0;
let totalMissed = 0; let totalMissed = 0;
let loggerTimer = Date.now(); 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) { for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
const slice = missingAudits.slice(i, i + BATCH_SIZE); const slice = missingAudits.slice(i, i + BATCH_SIZE);
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash))); const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
@@ -109,9 +109,11 @@ class AuditReplication {
version: 1, version: 1,
}); });
await blocksAuditsRepository.$saveAudit({ await blocksAuditsRepository.$saveAudit({
version: auditSummary.version || 0,
hash: blockHash, hash: blockHash,
height: auditSummary.height, height: auditSummary.height,
time: auditSummary.timestamp || auditSummary.time, time: auditSummary.timestamp || auditSummary.time,
unseenTxs: auditSummary.unseenTxs || [],
missingTxs: auditSummary.missingTxs || [], missingTxs: auditSummary.missingTxs || [],
addedTxs: auditSummary.addedTxs || [], addedTxs: auditSummary.addedTxs || [],
prioritizedTxs: auditSummary.prioritizedTxs || [], prioritizedTxs: auditSummary.prioritizedTxs || [],
@@ -192,6 +192,7 @@ class AccelerationRepository {
} }
} }
// modifies block transactions
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> { public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {}; const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
for (const tx of transactions) { for (const tx of transactions) {
@@ -1,13 +1,24 @@
import blocks from '../api/blocks';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; 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 { class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> { public async $saveAudit(audit: BlockAudit): Promise<void> {
try { try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight) 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.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), 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]); 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) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
@@ -62,24 +73,30 @@ class BlocksAuditRepositories {
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> { public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
try { try {
const [rows]: any[] = await DB.query( const [rows]: any[] = await DB.query(
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp, `SELECT
template, blocks_audits.version,
missing_txs as missingTxs, blocks_audits.height,
added_txs as addedTxs, blocks_audits.hash as id,
prioritized_txs as prioritizedTxs, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
fresh_txs as freshTxs, template,
sigop_txs as sigopTxs, unseen_txs as unseenTxs,
fullrbf_txs as fullrbfTxs, missing_txs as missingTxs,
accelerated_txs as acceleratedTxs, added_txs as addedTxs,
match_rate as matchRate, prioritized_txs as prioritizedTxs,
expected_fees as expectedFees, fresh_txs as freshTxs,
expected_weight as expectedWeight 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 FROM blocks_audits
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
WHERE blocks_audits.hash = ? WHERE blocks_audits.hash = ?
`, [hash]); `, [hash]);
if (rows.length) { if (rows.length) {
rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs);
rows[0].missingTxs = JSON.parse(rows[0].missingTxs); rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs); rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
@@ -101,7 +118,7 @@ class BlocksAuditRepositories {
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> { public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
try { try {
const blockAudit = await this.$getBlockAudit(hash); const blockAudit = await this.$getBlockAudit(hash);
if (blockAudit) { if (blockAudit) {
const isAdded = blockAudit.addedTxs.includes(txid); const isAdded = blockAudit.addedTxs.includes(txid);
const isPrioritized = blockAudit.prioritizedTxs.includes(txid); const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
@@ -115,16 +132,17 @@ class BlocksAuditRepositories {
firstSeen = tx.time; firstSeen = tx.time;
} }
}); });
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
return { return {
seen: isExpected || isPrioritized || isAccelerated, seen: wasSeen,
expected: isExpected, expected: isExpected,
added: isAdded, added: isAdded && (blockAudit.version === 0 || !wasSeen),
prioritized: isPrioritized, prioritized: isPrioritized,
conflict: isConflict, conflict: isConflict,
accelerated: isAccelerated, accelerated: isAccelerated,
firstSeen, firstSeen,
} };
} }
return null; return null;
} catch (e: any) { } catch (e: any) {
@@ -186,6 +204,96 @@ class BlocksAuditRepositories {
throw e; 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(); export default new BlocksAuditRepositories();
+1 -1
View File
@@ -144,7 +144,7 @@ __REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=14819
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]} __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# MEMPOOL_SERVICES # MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""} __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS # REDIS
+1 -1
View File
@@ -41,7 +41,7 @@ __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
__ACCELERATOR__=${ACCELERATOR:=false} __ACCELERATOR__=${ACCELERATOR:=false}
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true} __ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
__SERVICES_API__=${SERVICES_API:=false} __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
+1 -10
View File
@@ -543,16 +543,7 @@ describe('Mainnet', () => {
} }
}); });
cy.get('.alert').should('be.visible'); cy.get('.alert-replaced').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;
});
});
}); });
it('shows RBF transactions properly (desktop)', () => { it('shows RBF transactions properly (desktop)', () => {
@@ -750,7 +750,7 @@
}, },
"backendInfo": { "backendInfo": {
"hostname": "node205.tk7.mempool.space", "hostname": "node205.tk7.mempool.space",
"version": "3.0.0-dev", "version": "3.0.0-beta",
"gitCommit": "abbc8a134", "gitCommit": "abbc8a134",
"lightning": false "lightning": false
}, },
@@ -25,6 +25,7 @@
"HISTORICAL_PRICE": true, "HISTORICAL_PRICE": true,
"ADDITIONAL_CURRENCIES": false, "ADDITIONAL_CURRENCIES": false,
"ACCELERATOR": false, "ACCELERATOR": false,
"ACCELERATOR_BUTTON": true,
"PUBLIC_ACCELERATIONS": false, "PUBLIC_ACCELERATIONS": false,
"SERVICES_API": "https://mempool.space/api/v1/services" "SERVICES_API": "https://mempool.space/api/v1/services"
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "3.0.0-dev", "version": "3.0.0-beta",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "3.0.0-dev", "version": "3.0.0-beta",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "^17.3.1", "@angular-devkit/build-angular": "^17.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "3.0.0-dev", "version": "3.0.0-beta",
"description": "Bitcoin mempool visualizer and blockchain explorer backend", "description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space", "homepage": "https://mempool.space",
+14 -13
View File
@@ -9,6 +9,7 @@ import { StatusViewComponent } from './components/status-view/status-view.compon
import { AddressGroupComponent } from './components/address-group/address-group.component'; import { AddressGroupComponent } from './components/address-group/address-group.component';
import { TrackerComponent } from './components/tracker/tracker.component'; import { TrackerComponent } from './components/tracker/tracker.component';
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component'; import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from './route-guards';
const browserWindow = window || {}; const browserWindow = window || {};
// @ts-ignore // @ts-ignore
@@ -140,16 +141,17 @@ let routes: Routes = [
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{
path: 'tx',
canMatch: [TrackerGuard],
runGuardsAndResolvers: 'always',
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
},
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{
path: 'tracker',
data: { networkSpecific: true },
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
},
{ {
path: 'wallet', path: 'wallet',
children: [], children: [],
@@ -213,10 +215,6 @@ let routes: Routes = [
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{
path: '**',
redirectTo: ''
},
]; ];
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
@@ -301,13 +299,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{
path: '**',
redirectTo: ''
},
]; ];
} }
if (!window['isMempoolSpaceBuild']) {
routes.push({
path: '**',
redirectTo: ''
});
}
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, { imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabledBlocking', initialNavigation: 'enabledBlocking',
+1 -1
View File
@@ -151,7 +151,7 @@ export const languages: Language[] = [
{ code: 'fr', name: 'Français' }, // French { code: 'fr', name: 'Français' }, // French
// { code: 'gl', name: 'Galego' }, // Galician // { code: 'gl', name: 'Galego' }, // Galician
{ code: 'ko', name: '한국어' }, // Korean { code: 'ko', name: '한국어' }, // Korean
// { code: 'hr', name: 'Hrvatski' }, // Croatian { code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian // { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi { code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese { code: 'ne', name: 'नेपाली' }, // Nepalese
@@ -389,16 +389,30 @@
</div> </div>
} }
</div> </div>
@if (canPayWithCashapp) { @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center"> <div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
<p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p> <p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p>
</div> </div>
} }
} }
@if (canPayWithCashapp) { @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p> <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p>
<img class="paymentMethod mx-2" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')"> @if (canPayWithCashapp) {
<img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
}
@if (canPayWithApplePay) {
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> }
<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" (click)="moveToStep('googlepay')">
<img src="/resources/google-pay.png" height=37>
</div>
}
</div> </div>
} }
</div> </div>
@@ -421,9 +435,9 @@
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button> <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
</div> </div>
</div> </div>
} @else if (step === 'cashapp') { } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
<!-- Show checkout page --> <!-- Show checkout page -->
<div class="row mb-md-1 text-center"> <div class="row mb-md-1 text-center" id="confirm-title">
<div class="col-sm" id="confirm-payment-title"> <div class="col-sm" id="confirm-payment-title">
<h1 style="font-size: larger;"><ng-content select="[slot='checkout-title']"></ng-content><span class="default-slot" i18n="accelerator.confirm-your-payment">Confirm your payment</span></h1> <h1 style="font-size: larger;"><ng-content select="[slot='checkout-title']"></ng-content><span class="default-slot" i18n="accelerator.confirm-your-payment">Confirm your payment</span></h1>
</div> </div>
@@ -437,7 +451,7 @@
</div> </div>
</div> </div>
@if (!loadingCashapp) { @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
<div class="row text-center mt-1"> <div class="row text-center mt-1">
<div class="col-sm"> <div class="col-sm">
<div class="form-group w-100"> <div class="form-group w-100">
@@ -456,8 +470,14 @@
<div class="row text-center mt-1"> <div class="row text-center mt-1">
<div class="col-sm"> <div class="col-sm">
<div class="form-group w-100"> <div class="form-group w-100">
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div> @if (step === 'applepay') {
@if (loadingCashapp) { <div id="apple-pay-button" class="apple-pay-button apple-pay-button-black" style="height: 50px" [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'cashapp') {
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'googlepay') {
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
}
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
<div display="d-flex flex-row justify-content-center"> <div display="d-flex flex-row justify-content-center">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span> <span i18n="accelerator.loading-payment-method">Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div> <div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
@@ -505,7 +525,7 @@
<div class="col-sm"> <div class="col-sm">
<div class="d-flex flex-row flex-column justify-content-center align-items-center"> <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> <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> <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> <div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
@@ -549,7 +569,7 @@
<button type="button" *ngIf="advancedEnabled" class="btn btn-sm btn-outline-info btn-small-height ml-2" (click)="moveToStep('quote')" i18n="accelerator.customize">customize</button> <button type="button" *ngIf="advancedEnabled" class="btn btn-sm btn-outline-info btn-small-height ml-2" (click)="moveToStep('quote')" i18n="accelerator.customize">customize</button>
</ng-template> </ng-template>
<ng-template #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template> <ng-template id="accelerate-to" #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
<ng-template #accelerateButton> <ng-template #accelerateButton>
<div class="position-relative"> <div class="position-relative">
@@ -11,8 +11,7 @@
.paymentMethod { .paymentMethod {
padding: 10px; padding: 10px;
background-color: var(--secondary); background-color: var(--secondary);
border-radius: 15px; border-radius: 10px;
border: 2px solid var(--bg);
cursor: pointer; cursor: pointer;
} }
@@ -202,4 +201,19 @@
.btn-error-wrapper { .btn-error-wrapper {
height: 26px; height: 26px;
}
.apple-pay-button {
display: inline-block;
-webkit-appearance: -apple-pay-button;
-apple-pay-button-type: plain; /* Use any supported button type. */
}
.apple-pay-button-black {
-apple-pay-button-style: black;
}
.apple-pay-button-white {
-apple-pay-button-style: white;
}
.apple-pay-button-white-with-line {
-apple-pay-button-style: white-outline;
} }
@@ -1,7 +1,8 @@
/* eslint-disable no-console */
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '../../services/services-api.service'; import { ServicesApiServices } from '../../services/services-api.service';
import { nextRoundNumber, insecureRandomUUID } from '../../shared/common.utils'; import { md5, insecureRandomUUID } from '../../shared/common.utils';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '../../services/audio.service';
import { ETA, EtaService } from '../../services/eta.service'; import { ETA, EtaService } from '../../services/eta.service';
@@ -10,8 +11,9 @@ import { MiningStats } from '../../services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service'; import { IAuth, AuthServiceMempool } from '../../services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service'; import { EnterpriseService } from '../../services/enterprise.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
export type AccelerationEstimate = { export type AccelerationEstimate = {
hasAccess: boolean; hasAccess: boolean;
@@ -24,7 +26,7 @@ export type AccelerationEstimate = {
mempoolBaseFee: number; mempoolBaseFee: number;
vsizeFee: number; vsizeFee: number;
pools: number[]; pools: number[];
availablePaymentMethods: {[method: string]: {min: number, max: number}}; availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
unavailable?: boolean; unavailable?: boolean;
options: { // recommended bid options options: { // recommended bid options
fee: number; // recommended userBid in sats fee: number; // recommended userBid in sats
@@ -47,7 +49,7 @@ export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2; export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4; export const MAX_BID_RATIO = 4;
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing' | 'paid' | 'success'; type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
@Component({ @Component({
selector: 'app-accelerate-checkout', selector: 'app-accelerate-checkout',
@@ -61,6 +63,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() eta: ETA; @Input() eta: ETA;
@Input() scrollEvent: boolean; @Input() scrollEvent: boolean;
@Input() cashappEnabled: boolean = true; @Input() cashappEnabled: boolean = true;
@Input() applePayEnabled: boolean = false;
@Input() googlePayEnabled: boolean = true;
@Input() advancedEnabled: boolean = false; @Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false; @Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false; @Input() showDetails: boolean = false;
@@ -79,17 +83,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
timePaid: number = 0; // time acceleration requested timePaid: number = 0; // time acceleration requested
math = Math; math = Math;
isMobile: boolean = window.innerWidth <= 767.98; 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'; private _step: CheckoutStep = 'summary';
simpleMode: boolean = true; simpleMode: boolean = true;
paymentMethod: 'cashapp' | 'btcpay';
timeoutTimer: any; timeoutTimer: any;
authSubscription$: Subscription; authSubscription$: Subscription;
auth: IAuth | null = null; auth: IAuth | null = null;
// accelerator stuff // accelerator stuff
square: { appId: string, locationId: string};
accelerationUUID: string; accelerationUUID: string;
accelerationSubscription: Subscription; accelerationSubscription: Subscription;
difficultySubscription: Subscription; difficultySubscription: Subscription;
@@ -110,14 +119,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
// square // square
loadingCashapp = false; loadingCashapp = false;
cashappError = false; loadingApplePay = false;
cashappSubmit: any; loadingGooglePay = false;
payments: any; payments: any;
cashAppPay: any; cashAppPay: any;
cashAppSubscription: Subscription; applePay: any;
googlePay: any;
conversionsSubscription: Subscription; conversionsSubscription: Subscription;
conversions: any; conversions: Record<string, number>;
// btcpay // btcpay
loadingBtcpayInvoice = false; loadingBtcpayInvoice = false;
invoice = undefined; invoice = undefined;
@@ -133,9 +143,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private enterpriseService: EnterpriseService, private enterpriseService: EnterpriseService,
) { ) {
this.accelerationUUID = insecureRandomUUID(); this.accelerationUUID = insecureRandomUUID();
// Check if Apple Pay available
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
if (window['ApplePaySession']) {
this.applePayEnabled = true;
}
} }
ngOnInit() { ngOnInit(): void {
this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => { this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => {
if (this.auth?.user?.userId !== auth?.user?.userId) { if (this.auth?.user?.userId !== auth?.user?.userId) {
this.auth = auth; this.auth = auth;
@@ -160,13 +176,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.moveToStep('summary'); this.moveToStep('summary');
} }
this.servicesApiService.setupSquare$().subscribe(ids => {
this.square = {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
});
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@@ -174,7 +183,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
); );
} }
ngOnDestroy() { ngOnDestroy(): void {
if (this.estimateSubscription) { if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
} }
@@ -194,7 +203,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
} }
moveToStep(step: CheckoutStep) { moveToStep(step: CheckoutStep): void {
this._step = step; this._step = step;
if (this.timeoutTimer) { if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer); clearTimeout(this.timeoutTimer);
@@ -203,6 +212,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.fetchEstimate(); this.fetchEstimate();
} }
if (this._step === 'checkout') { if (this._step === 'checkout') {
this.insertSquare();
this.enterpriseService.goal(8); this.enterpriseService.goal(8);
} }
if (this._step === 'checkout' && this.canPayWithBitcoin) { if (this._step === 'checkout' && this.canPayWithBitcoin) {
@@ -212,15 +222,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.requestBTCPayInvoice(); this.requestBTCPayInvoice();
} else if (this._step === 'cashapp' && this.cashappEnabled) { } else if (this._step === 'cashapp' && this.cashappEnabled) {
this.loadingCashapp = true; this.loadingCashapp = true;
this.insertSquare();
this.setupSquare(); this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'applepay' && this.applePayEnabled) {
this.loadingApplePay = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'googlepay' && this.googlePayEnabled) {
this.loadingGooglePay = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'paid') { } else if (this._step === 'paid') {
this.timePaid = Date.now(); this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => { this.timeoutTimer = setTimeout(() => {
if (this.step === 'paid') { if (this.step === 'paid') {
this.accelerateError = 'internal_server_error'; this.accelerateError = 'internal_server_error';
} }
}, 120000) }, 120000);
} }
this.hasDetails.emit(this._step === 'quote'); this.hasDetails.emit(this._step === 'quote');
} }
@@ -231,14 +249,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
/** /**
* Scroll to element id with or without setTimeout * Scroll to element id with or without setTimeout
*/ */
scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void { scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void {
setTimeout(() => { setTimeout(() => {
this.scrollToElement(id, position); this.scrollToElement(id, position);
}, timeout); }, timeout);
} }
scrollToElement(id: string, position: ScrollLogicalPosition) { scrollToElement(id: string, position: ScrollLogicalPosition): void {
const acceleratePreviewAnchor = document.getElementById(id); const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) { if (acceleratePreviewAnchor) {
this.cd.markForCheck(); this.cd.markForCheck();
@@ -253,7 +271,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
/** /**
* Accelerator * Accelerator
*/ */
fetchEstimate() { fetchEstimate(): void {
if (this.estimateSubscription) { if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
} }
@@ -317,7 +335,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
}), }),
catchError((response) => { catchError(() => {
this.estimate = undefined; this.estimate = undefined;
this.quoteError = `cannot_accelerate_tx`; this.quoteError = `cannot_accelerate_tx`;
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
@@ -388,57 +406,248 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* Square * Square
*/ */
insertSquare(): void { insertSquare(): void {
//@ts-ignore if (!this.isProdDomain && !isDevMode()) {
if (window.Square) { return;
}
if (window['Square']) {
return; return;
} }
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js'; let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
if (document.location.hostname === 'mempool-staging.fmt.mempool.space' || if (this.isProdDomain) {
document.location.hostname === 'mempool-staging.va1.mempool.space' || statsUrl = '/square/v1/square.js';
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';
} }
(function() { (function(): void {
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
// @ts-ignore
g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s); g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s);
})(); })();
} }
setupSquare() { setupSquare(): void {
const init = () => { if (!this.isProdDomain && !isDevMode()) {
return;
}
const init = (): void => {
this.initSquare(); this.initSquare();
}; };
//@ts-ignore if (!window['Square']) {
if (!window.Square) { console.debug('Square.js failed to load properly. Retrying.');
console.debug('Square.js failed to load properly. Retrying in 1 second.'); setTimeout(this.setupSquare.bind(this), 100);
setTimeout(init, 1000);
} else { } else {
init(); init();
} }
} }
async initSquare(): Promise<void> { async initSquare(): Promise<void> {
try { try {
//@ts-ignore this.servicesApiService.setupSquare$().subscribe({
this.payments = window.Square.payments(this.square.appId, this.square.locationId) next: async (ids) => {
await this.requestCashAppPayment(); this.payments = window['Square'].payments(ids.squareAppId, ids.squareLocationId);
const urlParams = new URLSearchParams(window.location.search);
if (this._step === 'cashapp' || urlParams.get('cash_request_id')) {
await this.requestCashAppPayment();
} else if (this._step === 'applepay') {
await this.requestApplePayPayment();
} else if (this._step === 'googlepay') {
await this.requestGooglePayPayment();
}
},
error: () => {
console.debug('Error loading Square Payments');
this.accelerateError = 'cannot_setup_square';
}
});
} catch (e) { } catch (e) {
console.debug('Error loading Square Payments', e); console.debug('Error loading Square Payments', e);
this.cashappError = true; this.accelerateError = 'cannot_setup_square';
return;
} }
} }
async requestCashAppPayment() {
if (this.cashAppSubscription) { /**
this.cashAppSubscription.unsubscribe(); * APPLE PAY
} */
async requestApplePayPayment(): Promise<void> {
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
if (this.applePay) {
this.applePay.destroy();
}
const costUSD = this.cost / 100_000_000 * conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
total: {
amount: costUSD.toFixed(2),
label: 'Total',
},
});
try {
this.applePay = await this.payments.applePay(paymentRequest);
const applePayButton = document.getElementById('apple-pay-button');
if (!applePayButton) {
console.error(`Unable to find apple pay button id='apple-pay-button'`);
// Try again
setTimeout(this.requestApplePayPayment.bind(this), 500);
return;
}
this.loadingApplePay = false;
applePayButton.addEventListener('click', async event => {
event.preventDefault();
const tokenResult = await this.applePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithApplePay$(
this.tx.txid,
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID
).subscribe({
next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
this.applePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
}
});
} else {
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
}
});
} catch (e) {
console.error(e);
}
}
);
}
/**
* GOOGLE PAY
*/
async requestGooglePayPayment(): Promise<void> {
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
if (this.googlePay) {
this.googlePay.destroy();
}
const costUSD = this.cost / 100_000_000 * conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
total: {
amount: costUSD.toFixed(2),
label: 'Total'
}
});
this.googlePay = await this.payments.googlePay(paymentRequest , {
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
});
await this.googlePay.attach(`#google-pay-button`, {
buttonType: 'pay',
buttonSizeMode: 'fill',
});
this.loadingGooglePay = false;
document.getElementById('google-pay-button').addEventListener('click', async event => {
event.preventDefault();
const tokenResult = await this.googlePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid,
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID
).subscribe({
next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
this.googlePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
}
});
} else {
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
}
});
}
);
}
/**
* CASHAPP
*/
async requestCashAppPayment(): Promise<void> {
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@@ -452,42 +661,37 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
countryCode: 'US', countryCode: 'US',
currencyCode: 'USD', currencyCode: 'USD',
total: { total: {
amount: costUSD.toString(), amount: costUSD.toFixed(2),
label: 'Total', label: 'Total',
pending: true, pending: true,
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`, productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
}, }
button: { shape: 'semiround', size: 'small', theme: 'light'}
}); });
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, { 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)}`, referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
button: { shape: 'semiround', size: 'small', theme: 'light'}
}); });
if (this.step === 'cashapp') { await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
}
this.loadingCashapp = false; this.loadingCashapp = false;
const that = this; this.cashAppPay.addEventListener('ontokenization', event => {
this.cashAppPay.addEventListener('ontokenization', function (event) {
const { tokenResult, error } = event.detail; const { tokenResult, error } = event.detail;
if (error) { if (error) {
this.accelerateError = error; this.accelerateError = error;
} else if (tokenResult.status === 'OK') { } else if (tokenResult.status === 'OK') {
that.servicesApiService.accelerateWithCashApp$( this.servicesApiService.accelerateWithCashApp$(
that.tx.txid, this.tx.txid,
tokenResult.token, tokenResult.token,
tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId, tokenResult.details.cashAppPay.referenceId,
that.accelerationUUID this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
that.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
if (that.cashAppPay) { if (this.cashAppPay) {
that.cashAppPay.destroy(); this.cashAppPay.destroy();
} }
setTimeout(() => { setTimeout(() => {
this.moveToStep('paid'); this.moveToStep('paid');
@@ -498,7 +702,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
that.accelerateError = response.error; this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {
// Reset everything by reloading the page :D, can be improved // Reset everything by reloading the page :D, can be improved
@@ -517,7 +721,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
/** /**
* BTCPay * BTCPay
*/ */
async requestBTCPayInvoice() { async requestBTCPayInvoice(): Promise<void> {
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe( this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => { switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId); return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
@@ -547,47 +751,61 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
/** /**
* UI events * UI events
*/ */
selectedOptionChanged(event) { selectedOptionChanged(event): void {
this.selectedOption = event.target.id; this.selectedOption = event.target.id;
} }
get step() { get step(): CheckoutStep {
return this._step; return this._step;
} }
get paymentMethods() { get paymentMethods(): PaymentMethod[] {
return Object.keys(this.estimate?.availablePaymentMethods || {}); return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[];
} }
get couldPayWithBitcoin() { get couldPayWithBitcoin(): boolean {
return !!this.estimate?.availablePaymentMethods?.bitcoin; return !!this.estimate?.availablePaymentMethods?.bitcoin;
} }
get couldPayWithCashapp() { get couldPayWithCashapp(): boolean {
if (!this.cashappEnabled) { if (!this.cashappEnabled) {
return false; return false;
} }
return !!this.estimate?.availablePaymentMethods?.cashapp; return !!this.estimate?.availablePaymentMethods?.cashapp;
} }
get couldPayWithBalance() { get couldPayWithApplePay(): boolean {
if (!this.applePayEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.applePay;
}
get couldPayWithGooglePay(): boolean {
if (!this.googlePayEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.googlePay;
}
get couldPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) { if (!this.hasAccessToBalanceMode) {
return false; return false;
} }
return !!this.estimate?.availablePaymentMethods?.balance; return !!this.estimate?.availablePaymentMethods?.balance;
} }
get couldPay() { get couldPay(): boolean {
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp; return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay || this.couldPayWithGooglePay;
} }
get canPayWithBitcoin() { get canPayWithBitcoin(): boolean {
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin; const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max; return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
} }
get canPayWithCashapp() { get canPayWithCashapp(): boolean {
if (!this.cashappEnabled || !this.conversions) { if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false; return false;
} }
@@ -598,11 +816,43 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return true; return true;
} }
} }
return false; return false;
} }
get canPayWithBalance() { get canPayWithApplePay(): boolean {
if (!this.applePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.applePay;
if (paymentMethod) {
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
return true;
}
}
return false;
}
get canPayWithGooglePay(): boolean {
if (!this.googlePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.googlePay;
if (paymentMethod) {
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
return true;
}
}
return false;
}
get canPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) { if (!this.hasAccessToBalanceMode) {
return false; return false;
} }
@@ -610,11 +860,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max && this.cost <= this.estimate?.userBalance; return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max && this.cost <= this.estimate?.userBalance;
} }
get canPay() { get canPay(): boolean {
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp; return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay || this.canPayWithGooglePay;
} }
get hasAccessToBalanceMode() { get hasAccessToBalanceMode(): boolean {
return this.isLoggedIn() && this.estimate?.hasAccess; return this.isLoggedIn() && this.estimate?.hasAccess;
} }
@@ -0,0 +1,62 @@
<div
#tooltip
*ngIf="accelerationInfo && tooltipPosition !== null"
class="acceleration-tooltip"
[style.left]="tooltipPosition.x + 'px'"
[style.top]="tooltipPosition.y + 'px'"
>
<table>
<tbody>
<tr>
<td class="label" i18n="transaction.status|Transaction Status">Status</td>
<td class="value">
@if (accelerationInfo.status === 'seen') {
<span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span>
} @else if (accelerationInfo.status === 'accelerated') {
<span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
} @else if (accelerationInfo.status === 'mined') {
<span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
}
</td>
</tr>
<tr *ngIf="accelerationInfo.fee">
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
</tr>
<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 class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
} @else {
<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|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 oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
} @else {
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
}
}
</tr>
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td class="value" *ngIf="accelerationInfo.pools">
<ng-container *ngFor="let pool of accelerationInfo.pools">
<img *ngIf="accelerationInfo.poolsData[pool]"
class="pool-logo"
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'"
[alt]="'Logo of ' + pool.name + ' mining pool'">
</ng-container>
</td>
</tr>
</tbody>
</table>
</div>
@@ -0,0 +1,39 @@
.acceleration-tooltip {
position: fixed;
z-index: 3;
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: var(--tooltip-grey);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px 15px;
text-align: left;
pointer-events: none;
.badge.badge-accelerated {
background-color: var(--tertiary);
color: white;
}
.value {
text-align: end;
}
.label {
padding-right: 30px;
}
.pool-logo {
width: 22px;
height: 22px;
position: relative;
top: -1px;
margin-right: 3px;
}
.oobFees {
color: #905cf4;
}
}
@@ -0,0 +1,38 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
@Component({
selector: 'app-acceleration-timeline-tooltip',
templateUrl: './acceleration-timeline-tooltip.component.html',
styleUrls: ['./acceleration-timeline-tooltip.component.scss'],
})
export class AccelerationTimelineTooltipComponent implements OnChanges {
@Input() accelerationInfo: any;
@Input() cursorPosition: { x: number, y: number };
tooltipPosition: any = null;
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
constructor() {}
ngOnChanges(changes): void {
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
let x = Math.max(10, changes.cursorPosition.currentValue.x - 50);
let y = changes.cursorPosition.currentValue.y + 20;
if (this.tooltipElement) {
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
if ((x + elementBounds.width) > (window.innerWidth - 10)) {
x = Math.max(0, window.innerWidth - elementBounds.width - 10);
}
if (y + elementBounds.height > (window.innerHeight - 20)) {
y = y - elementBounds.height - 20;
}
}
this.tooltipPosition = { x, y };
}
}
hasPoolsData(): boolean {
return Object.keys(this.accelerationInfo.poolsData).length > 0;
}
}
@@ -26,7 +26,7 @@
<div class="node" [id]="'confirmed'"> <div class="node" [id]="'confirmed'">
<div class="acc-to-confirmed left go-faster"></div> <div class="acc-to-confirmed left go-faster"></div>
<div class="shape-border waiting"> <div class="shape-border waiting">
<div class="shape animate"></div> <div class="shape"></div>
</div> </div>
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div> <div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
</div> </div>
@@ -58,7 +58,7 @@
<div class="nodes"> <div class="nodes">
<div class="node" [id]="'first-seen'"> <div class="node" [id]="'first-seen'">
<div class="seen-to-acc right"></div> <div class="seen-to-acc right"></div>
<div class="shape-border"> <div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);">
<div class="shape"></div> <div class="shape"></div>
</div> </div>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div> <div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
@@ -80,7 +80,7 @@
} @else { } @else {
<div class="seen-to-acc right"></div> <div class="seen-to-acc right"></div>
} }
<div class="shape-border"> <div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
<div class="shape"></div> <div class="shape"></div>
@if (!tx.status.confirmed) { @if (!tx.status.confirmed) {
<div class="connector down loading"></div> <div class="connector down loading"></div>
@@ -89,14 +89,14 @@
@if (tx.status.confirmed) { @if (tx.status.confirmed) {
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div> <div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
} }
<div class="time offset-left" [class.no-margin]="!tx.status.confirmed"> <div class="time" [class.no-margin]="!tx.status.confirmed" [class.offset-left]="!tx.status.confirmed">
@if (!tx.status.confirmed) { @if (!tx.status.confirmed) {
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }} <span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
} }
@if (useAbsoluteTime) { @if (useAbsoluteTime) {
<span>{{ acceleratedAt * 1000 | date }}</span> <span>{{ acceleratedAt * 1000 | date }}</span>
} @else { } @else {
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="true"></app-time> <app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed"></app-time>
} }
</div> </div>
</div> </div>
@@ -113,7 +113,10 @@
} @else { } @else {
<div class="seen-to-acc left"></div> <div class="seen-to-acc left"></div>
} }
<div class="shape-border" [class.waiting]="!tx.status.confirmed"> <div class="shape-border"
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}"
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
(pointerout)="onBlur($event);">
<div class="shape"></div> <div class="shape"></div>
</div> </div>
@if (tx.status.confirmed) { @if (tx.status.confirmed) {
@@ -130,4 +133,10 @@
</div> </div>
</div> </div>
</div> </div>
<app-acceleration-timeline-tooltip
[accelerationInfo]="hoverInfo"
[cursorPosition]="tooltipPosition"
></app-acceleration-timeline-tooltip>
</div> </div>
@@ -152,9 +152,16 @@
margin-bottom: -8px; margin-bottom: -8px;
transform: translateY(-50%); transform: translateY(-50%);
border-radius: 50%; border-radius: 50%;
cursor: pointer;
padding: 4px; padding: 4px;
background: transparent; background: transparent;
transition: background-color 300ms, padding 300ms;
&.hovering {
cursor: pointer;
&:hover {
padding: 0px;
}
}
.shape { .shape {
position: relative; position: relative;
@@ -1,6 +1,8 @@
import { Component, Input, OnInit, OnChanges } from '@angular/core'; import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
import { ETA } from '../../services/eta.service'; import { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
import { MiningService } from '../../services/mining.service';
@Component({ @Component({
selector: 'app-acceleration-timeline', selector: 'app-acceleration-timeline',
@@ -10,6 +12,7 @@ import { Transaction } from '../../interfaces/electrs.interface';
export class AccelerationTimelineComponent implements OnInit, OnChanges { export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number; @Input() transactionTime: number;
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() eta: ETA; @Input() eta: ETA;
// A mined transaction has standard ETA and accelerated ETA undefined // A mined transaction has standard ETA and accelerated ETA undefined
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet) // A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
@@ -22,13 +25,25 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
useAbsoluteTime: boolean = false; useAbsoluteTime: boolean = false;
interval: number; interval: number;
constructor() {} tooltipPosition = null;
hoverInfo: any = null;
poolsData: { [id: number]: SinglePoolStats } = {};
constructor(
private miningService: MiningService,
) {}
ngOnInit(): void { ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000; this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
this.now = Math.floor(new Date().getTime() / 1000); this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600; this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
this.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.poolsData[pool.unique_id] = pool;
}
});
this.interval = window.setInterval(() => { this.interval = window.setInterval(() => {
this.now = Math.floor(new Date().getTime() / 1000); this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600; this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
@@ -52,4 +67,42 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
ngOnDestroy(): void { ngOnDestroy(): void {
clearInterval(this.interval); clearInterval(this.interval);
} }
onHover(event, status: string): void {
if (status === 'seen') {
this.hoverInfo = {
status,
fee: this.tx.fee,
weight: this.tx.weight
};
} else if (status === 'accelerated') {
this.hoverInfo = {
status,
fee: this.accelerationInfo?.effectiveFee || this.tx.fee,
weight: this.tx.weight,
feeDelta: this.accelerationInfo?.feeDelta || this.tx.feeDelta,
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
poolsData: this.poolsData
};
} else if (status === 'mined') {
this.hoverInfo = {
status,
fee: this.accelerationInfo?.effectiveFee,
weight: this.tx.weight,
bidBoost: this.accelerationInfo?.bidBoost,
minedByPoolUniqueId: this.accelerationInfo?.minedByPoolUniqueId,
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
poolsData: this.poolsData
};
}
}
onBlur(event): void {
this.hoverInfo = null;
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
}
} }
@@ -23,7 +23,7 @@ import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -4,8 +4,8 @@
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations"> <div class="acceleration-list">
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed"> <table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
<thead> <thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th> <th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
<ng-container *ngIf="pending"> <ng-container *ngIf="pending">
@@ -16,11 +16,12 @@
<ng-container *ngIf="!pending"> <ng-container *ngIf="!pending">
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th> <th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="block text-right" i18n="shared.block-title">Block</th> <th class="block text-right" i18n="shared.block-title">Block</th>
<th class="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th> <th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th> <th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
</ng-container> </ng-container>
</thead> </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;"> <tr *ngFor="let acceleration of accelerations; let i= index;">
<td class="txid text-left"> <td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]"> <a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
@@ -49,10 +50,21 @@
<a *ngIf="acceleration.blockHeight" [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a> <a *ngIf="acceleration.blockHeight" [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a>
<span *ngIf="!acceleration.blockHeight">~</span> <span *ngIf="!acceleration.blockHeight">~</span>
</td> </td>
<td class="pool text-right" *ngIf="!this.widget">
@if (acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]) {
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pools[acceleration.minedByPoolUniqueId].slug]" class="badge" style="color: #FFF;padding:0;">
<img class="pool-logo" [src]="'/resources/mining-pools/' + pools[acceleration.minedByPoolUniqueId].slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pools[acceleration.minedByPoolUniqueId].name + ' mining pool'">
{{ pools[acceleration.minedByPoolUniqueId].name }}
</a>
} @else {
~
}
</td>
<td class="status text-right"> <td class="status text-right">
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span> <span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
<span *ngIf="acceleration.status.includes('completed')" class="badge badge-success" i18n="">Completed <span *ngIf="acceleration.status === 'completed_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('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="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>
<td class="date text-right" *ngIf="!this.widget"> <td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time> <app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@@ -61,22 +73,47 @@
</tr> </tr>
</tbody> </tbody>
<ng-template #skeleton> <ng-template #skeleton>
<tbody> @if (!pending) {
<tr *ngFor="let item of skeletonLines"> <tbody>
<td class="txid text-left"> <tr *ngFor="let item of skeletonLines">
<span class="skeleton-loader" style="max-width: 75px"></span> <td class="txid text-left">
</td> <span class="skeleton-loader" style="max-width: 200px"></span>
<td class="fee text-right"> </td>
<span class="skeleton-loader" style="max-width: 75px"></span> <td class="fee text-right">
</td> <span class="skeleton-loader" style="max-width: 100px"></span>
<td class="fee-delta text-right"> </td>
<span class="skeleton-loader" style="max-width: 75px"></span> <td class="block text-right">
</td> <span class="skeleton-loader" style="max-width: 100px"></span>
<td class="status text-right"> </td>
<span class="skeleton-loader" style="max-width: 75px"></span> <td class="pool text-right" *ngIf="!this.widget">
</td> <span class="skeleton-loader" style="max-width: 100px"></span>
</tr> </td>
</tbody> <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> </ng-template>
</table> </table>
@@ -12,7 +12,7 @@
padding-bottom: 0px; padding-bottom: 0px;
} }
.container-xl.legacy { .container-xl.legacy {
max-width: 1140px; max-width: 1200px;
} }
.container-xl.widget-container { .container-xl.widget-container {
min-height: 335px; min-height: 335px;
@@ -72,9 +72,25 @@ tr, td, th {
.block { .block {
width: 15%; width: 15%;
@media (max-width: 900px) {
display: none;
}
}
.pool {
width: 15%;
@media (max-width: 700px) { @media (max-width: 700px) {
display: none; display: none;
} }
.pool-logo {
width: 18px;
height: 18px;
position: relative;
top: -1px;
margin-right: 2px;
}
} }
.status { .status {
@@ -1,11 +1,12 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '../../../services/websocket.service';
import { ServicesApiServices } from '../../../services/services-api.service'; import { ServicesApiServices } from '../../../services/services-api.service';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '../../../services/seo.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MiningService } from '../../../services/mining.service';
@Component({ @Component({
selector: 'app-accelerations-list', selector: 'app-accelerations-list',
@@ -30,11 +31,14 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
keyNavigationSubscription: Subscription; keyNavigationSubscription: Subscription;
dir: 'rtl' | 'ltr' = 'ltr'; dir: 'rtl' | 'ltr' = 'ltr';
paramSubscription: Subscription; paramSubscription: Subscription;
pools: { [id: number]: SinglePoolStats } = {};
nonEmptyAccelerations: boolean = true;
constructor( constructor(
private servicesApiService: ServicesApiServices, private servicesApiService: ServicesApiServices,
private websocketService: WebsocketService, private websocketService: WebsocketService,
public stateService: StateService, public stateService: StateService,
private miningService: MiningService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private seoService: SeoService, private seoService: SeoService,
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -47,6 +51,12 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.pools[pool.unique_id] = pool;
}
});
if (!this.widget) { if (!this.widget) {
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`); this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
@@ -106,6 +116,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
for (const acc of accelerations) { for (const acc of accelerations) {
acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost; acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost;
} }
this.nonEmptyAccelerations = accelerations.length > 0;
if (this.widget) { if (this.widget) {
return of(accelerations.slice(0, 6)); return of(accelerations.slice(0, 6));
} else { } else {
@@ -1,7 +1,7 @@
@if (chartOnly) { @if (chartOnly) {
<ng-container *ngTemplateOutlet="pieChart"></ng-container> <ng-container *ngTemplateOutlet="pieChart"></ng-container>
} @else { } @else {
<table> <table style="width: 100%;">
<tbody> <tbody>
<tr> <tr>
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td> <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' : ''"> <td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<div class="effective-fee-container"> <div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) { @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 { } @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate> <app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
} }
</div> </div>
</td> </td>
@@ -61,4 +61,8 @@
& > div, & > div > svg { & > div, & > div > svg {
overflow: visible !important; overflow: visible !important;
} }
}
.oobFees {
color: #905cf4;
} }
@@ -67,12 +67,13 @@ export class ActiveAccelerationBox implements OnChanges {
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate); 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); const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
const lightenStep = acceleratingPools.length ? (0.48 / acceleratingPools.length) : 0;
acceleratingPools.forEach((poolId, index) => { acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId]; const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
data.push(getDataItem( data.push(getDataItem(
pool.lastEstimatedHashrate, pool.lastEstimatedHashrate,
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)), toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)),
`<b style="color: white">${pool.name} (${poolShare}%)</b>`, `<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true, true,
) as PieSeriesOption); ) 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();
});
}
}
}
@@ -30,7 +30,7 @@ const periodSeconds = {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -249,7 +249,7 @@
</ng-template> </ng-template>
<ng-template #pendingBalanceRow> <ng-template #pendingBalanceRow>
<td i18n="address.unconfirmed-balance" class="font-italic">Unconfirmed balance</td> <td i18n="accelerator.pending-state" class="font-italic">Pending</td>
<td *ngIf="mempoolStats.funded_txo_sum !== undefined; else confidentialTd" class="font-italic wrap-cell"><app-amount [satoshis]="mempoolStats.balance" [noFiat]="true" [addPlus]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolStats.balance"></app-fiat></span></td> <td *ngIf="mempoolStats.funded_txo_sum !== undefined; else confidentialTd" class="font-italic wrap-cell"><app-amount [satoshis]="mempoolStats.balance" [noFiat]="true" [addPlus]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolStats.balance"></app-fiat></span></td>
</ng-template> </ng-template>
@@ -259,7 +259,7 @@
</ng-template> </ng-template>
<ng-template #pendingUtxoRow> <ng-template #pendingUtxoRow>
<td i18n="address.unconfirmed-utxos" class="font-italic">Unconfirmed UTXOs</td> <td i18n="address.pending-utxos" class="font-italic">Pending UTXOs</td>
<td class="font-italic wrap-cell">{{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }}</td> <td class="font-italic wrap-cell">{{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }}</td>
</ng-template> </ng-template>
@@ -23,7 +23,7 @@ import { ActivatedRoute, Router } from '@angular/router';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -24,7 +24,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -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"> <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"> <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">
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon> <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
</a> </a>
<div class="filter-bar"> <div class="filter-bar">
@@ -24,7 +24,6 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
float: right;
&:hover, &:active { &:hover, &:active {
text-decoration: none; text-decoration: none;
@@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -2,10 +2,12 @@
<div class="graph-alignment" [class.grid-align]="!autofit" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'"> <div class="graph-alignment" [class.grid-align]="!autofit" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
<div class="block-overview-graph"> <div class="block-overview-graph">
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas> <canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable"> @if (!disableSpinner) {
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div> <div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable">
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div> <div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
</div> <div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div>
}
<app-block-overview-tooltip <app-block-overview-tooltip
[tx]="selectedTx || hoverTx" [tx]="selectedTx || hoverTx"
[cursorPosition]="tooltipPosition" [cursorPosition]="tooltipPosition"
@@ -18,6 +18,7 @@ const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity), censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity), missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
added: setOpacity(defaultAuditColors.added, unmatchedOpacity), added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity), prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity), accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
}; };
@@ -25,6 +26,7 @@ const unmatchedContrastAuditColors = {
censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity), censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity),
missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity), missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity),
added: setOpacity(contrastAuditColors.added, unmatchedOpacity), added: setOpacity(contrastAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity), prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity), accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity),
}; };
@@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
flags: number; flags: number;
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n; bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
time?: number; time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
scene?: BlockScene; scene?: BlockScene;
@@ -71,6 +71,7 @@ export const defaultAuditColors = {
censored: hexToColor('f344df'), censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'), added: hexToColor('0099ff'),
added_prioritized: darken(desaturate(hexToColor('0099ff'), 0.15), 0.85),
prioritized: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), prioritized: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
accelerated: hexToColor('8f5ff6'), accelerated: hexToColor('8f5ff6'),
}; };
@@ -101,6 +102,7 @@ export const contrastAuditColors = {
censored: hexToColor('ffa8ff'), censored: hexToColor('ffa8ff'),
missing: darken(desaturate(hexToColor('ffa8ff'), 0.3), 0.7), missing: darken(desaturate(hexToColor('ffa8ff'), 0.3), 0.7),
added: hexToColor('00bb98'), added: hexToColor('00bb98'),
added_prioritized: darken(desaturate(hexToColor('00bb98'), 0.15), 0.85),
prioritized: darken(desaturate(hexToColor('00bb98'), 0.3), 0.7), prioritized: darken(desaturate(hexToColor('00bb98'), 0.3), 0.7),
accelerated: hexToColor('8f5ff6'), accelerated: hexToColor('8f5ff6'),
}; };
@@ -136,6 +138,8 @@ export function defaultColorFunction(
return auditColors.missing; return auditColors.missing;
case 'added': case 'added':
return auditColors.added; return auditColors.added;
case 'added_prioritized':
return auditColors.added_prioritized;
case 'prioritized': case 'prioritized':
return auditColors.prioritized; return auditColors.prioritized;
case 'selected': case 'selected':
@@ -51,7 +51,7 @@
<tr *ngIf="hasEffectiveRate && effectiveRate != null"> <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.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 *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> <app-fee-rate [fee]="effectiveRate"></app-fee-rate>
</td> </td>
</tr> </tr>
@@ -75,6 +75,10 @@
<span *ngSwitchCase="'freshcpfp'" class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span> <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="'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> <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="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span> <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="'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> <span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
@@ -27,6 +27,9 @@ th, td {
width: 70%; width: 70%;
text-align: end; text-align: end;
} }
&.oobFees {
color: #905cf4;
}
} }
.badge.badge-accelerated { .badge.badge-accelerated {
@@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -521,6 +521,7 @@ export class BlockComponent implements OnInit, OnDestroy {
if (transactions && blockAudit) { if (transactions && blockAudit) {
const inTemplate = {}; const inTemplate = {};
const inBlock = {}; const inBlock = {};
const isUnseen = {};
const isAdded = {}; const isAdded = {};
const isPrioritized = {}; const isPrioritized = {};
const isCensored = {}; const isCensored = {};
@@ -543,6 +544,9 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const tx of transactions) { for (const tx of transactions) {
inBlock[tx.txid] = true; inBlock[tx.txid] = true;
} }
for (const txid of blockAudit.unseenTxs || []) {
isUnseen[txid] = true;
}
for (const txid of blockAudit.addedTxs) { for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true; isAdded[txid] = true;
} }
@@ -592,18 +596,27 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = 'accelerated'; 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'; tx.context = 'actual';
if (index === 0) { if (index === 0) {
tx.status = null; tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (isPrioritized[tx.txid]) { } 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 (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) { } else if (inTemplate[tx.txid]) {
anySeen = true;
tx.status = 'found'; tx.status = 'found';
} else if (isRbf[tx.txid]) { } else if (isRbf[tx.txid]) {
tx.status = 'rbf'; tx.status = 'rbf';
} else if (isUnseen[tx.txid] && anySeen) {
tx.status = 'added';
} else { } else {
tx.status = 'selected'; tx.status = 'selected';
isSelected[tx.txid] = true; isSelected[tx.txid] = true;
@@ -15,7 +15,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
}) })
@@ -23,7 +23,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -59,7 +59,7 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
.loadingGraphs.widget { .loadingGraphs.widget {
top: 75%; top: 75%;
@@ -28,7 +28,7 @@ interface Hashrate {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -17,7 +17,7 @@ const OUTLIERS_MEDIAN_MULTIPLIER = 4;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 16px); left: calc(50% - 16px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -11,7 +11,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 16px); left: calc(50% - 16px);
z-index: 100; z-index: 99;
} }
`], `],
templateUrl: './lbtc-pegs-graph.component.html', templateUrl: './lbtc-pegs-graph.component.html',
@@ -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['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['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['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> <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 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> <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>
@@ -2,5 +2,5 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 16px); left: calc(50% - 16px);
z-index: 100; z-index: 99;
} }
@@ -1,7 +1,7 @@
.sticky-loading { .sticky-loading {
position: absolute; position: absolute;
right: 10px; right: 10px;
z-index: 100; z-index: 99;
font-size: 14px; font-size: 14px;
@media (width >= 992px) { @media (width >= 992px) {
left: 32px; left: 32px;
@@ -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.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.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.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['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> <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> </div>
@@ -51,7 +51,8 @@
</div> </div>
</ng-template> </ng-template>
</div> </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> </div>
</ng-container> </ng-container>
@@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
import { Subscription, Observable, of, combineLatest } from 'rxjs'; import { Subscription, Observable, of, combineLatest } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface'; import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
@@ -77,6 +77,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
maxArrowPosition = 0; maxArrowPosition = 0;
rightPosition = 0; rightPosition = 0;
transition = 'background 2s, right 2s, transform 1s'; transition = 'background 2s, right 2s, transform 1s';
@ViewChild('arrowUp')
arrowElement: ElementRef<HTMLDivElement>;
acceleratingArrow: boolean = false;
markIndex: number; markIndex: number;
txPosition: MempoolPosition; txPosition: MempoolPosition;
@@ -201,6 +204,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.markBlocksSubscription = this.stateService.markBlock$ this.markBlocksSubscription = this.stateService.markBlock$
.subscribe((state) => { .subscribe((state) => {
const oldTxPosition = this.txPosition;
this.markIndex = undefined; this.markIndex = undefined;
this.txPosition = undefined; this.txPosition = undefined;
this.txFeePerVSize = undefined; this.txFeePerVSize = undefined;
@@ -209,6 +213,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
if (state.mempoolPosition) { if (state.mempoolPosition) {
this.txPosition = state.mempoolPosition; this.txPosition = state.mempoolPosition;
if (this.txPosition.accelerated && !oldTxPosition.accelerated) {
this.acceleratingArrow = true;
setTimeout(() => {
this.acceleratingArrow = false;
}, 2000);
}
} }
if (state.txFeePerVSize) { if (state.txFeePerVSize) {
this.txFeePerVSize = state.txFeePerVSize; this.txFeePerVSize = state.txFeePerVSize;
@@ -18,7 +18,7 @@ import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/grap
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 16px); left: calc(50% - 16px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -60,7 +60,7 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
.pool-distribution { .pool-distribution {
@@ -167,7 +167,7 @@ div.scrollable {
.loadingGraphs { .loadingGraphs {
position: absolute; position: absolute;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
top: 475px; top: 475px;
@media (max-width: 992px) { @media (max-width: 992px) {
top: 600px; top: 600px;
@@ -5,7 +5,7 @@
<br><br> <br><br>
<h2>Privacy Policy</h2> <h2>Privacy Policy</h2>
<h6>Updated: July 10, 2024</h6> <h6>Updated: July 31, 2024</h6>
<br><br> <br><br>
@@ -27,7 +27,7 @@
<br> <br>
<h4>General</h4> <h4>USING THIS WEBSITE</h4>
<p *ngIf="officialMempoolSpace">Out of respect for the Bitcoin community, this Website does not use any third-party analytics, third-party trackers, or third-party cookies, and we do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, we self-host this Website on our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this Website.</p> <p *ngIf="officialMempoolSpace">Out of respect for the Bitcoin community, this Website does not use any third-party analytics, third-party trackers, or third-party cookies, and we do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, we self-host this Website on our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this Website.</p>
@@ -35,7 +35,7 @@
<ul> <ul>
<li>We use basic webserver logging (nginx) for sysadmin purposes, which collects your IP address along with the requests you make. These logs are deleted after 10 days, and we do not share this data with any third-party. To conceal your IP address from our webserver logs, we recommend that you use Tor Browser with our Tor v3 hidden service onion hostname.</li> <li>We use basic webserver logging (nginx) for sysadmin purposes, which collects your IP address along with the requests you make. These logs are deleted after 10 days, and we do not share this data with any third-party.</li>
<br> <br>
@@ -49,7 +49,7 @@
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator&trade; your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p> <p *ngIf="officialMempoolSpace">If you use Mempool Accelerator&trade; your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
<p *ngIf="!officialMempoolSpace">If you click the accelerate button on a transaction you will load acceleration pricing information from Mempool. If you make an acceleration request, the TXID and your maximum bid will be sent to Mempool who will store and share this information with their mining pool partners, and publicly display accelerated transaction details on mempool.space and via Mempool's APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p> <p *ngIf="!officialMempoolSpace">When using Mempool Accelerator&trade; the mempool.space privacy policy will apply: <a href="https://mempool.space/privacy-policy">https://mempool.space/privacy-policy</a>.</p>
<br> <br>
@@ -74,22 +74,38 @@
<br> <br>
</ng-container> </ng-container>
<ng-container *ngIf="officialMempoolSpace">
<h4>PAYMENTS AND DONATIONS</h4> <h4>PAYMENTS AND DONATIONS</h4>
<p>If you make any payment to Mempool or donation to The Mempool Open Source Project&reg;, we may collect the following:</p> <p>If you make any payment to Mempool or donation to The Mempool Open Source Project&reg;, we may collect the following:</p>
<ul> <ul>
<li>Your e-mail address and/or country; we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as necessary for our fiat payment processor.</li> <li>Your e-mail address and/or country; we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as necessary for our fiat payment processor.</li>
<li>If you make a payment using Bitcoin, we will process your payment using our self-hosted BTCPay Server instance. We will not share your payment details with any third-party. For payments made over the Lightning network, we may utilize third party LSPs / lightning liquidity providers.</li> <li>If you make a payment using Bitcoin, we will process your payment using our self-hosted BTCPay Server instance. We will not share your payment details with any third-party. For payments made over the Lightning network, we may utilize third party LSPs / lightning liquidity providers.</li>
<li>If you make a payment using Fiat we will collect your payment details. We will share your payment details with our fiat payment processor Square (Block, Inc.),. - Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy.</li> <li>If you make a payment using Fiat we will collect your payment details. We will share your payment details with our fiat payment processor Square (Block, Inc.) - Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy.</li>
</ul> </ul>
<br> <br>
</ng-container>
<ng-container *ngIf="officialMempoolSpace">
<h4>DATA RETENTION AND ACCOUNT INACTIVITY</h4>
<p>We aim to retain your data only as long as necessary:</p>
<ul>
<li>An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator™ Pro account credit</li>
<li>If an account meets the criteria for inactivity as defined above, we will automatically delete the associated account data after a period of 6 months of continuous inactivity, except in the case of payment disputes or account irregularities.</li>
</ul>
</ng-container>
<p>EOF</p> <p>EOF</p>
@@ -46,7 +46,7 @@
@if (replaced) { @if (replaced) {
<div class="alert-replaced" role="alert"> <div class="alert-replaced" role="alert">
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span> <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> </div>
} @else { } @else {
<div class="tracker-bar"> <div class="tracker-bar">
@@ -61,7 +61,7 @@
@if (transactionTime > 0) { @if (transactionTime > 0) {
<i><app-time kind="since" [time]="transactionTime" [fastRender]="true" [showTooltip]="true"></app-time></i> <i><app-time kind="since" [time]="transactionTime" [fastRender]="true" [showTooltip]="true"></app-time></i>
} @else { } @else {
<span class="skeleton-loader" style="max-width: 50%;"></span> <span class="skeleton-loader" style="max-width: 200px;"></span>
} }
</div> </div>
</div> </div>
@@ -78,7 +78,7 @@
</span> </span>
</ng-container> </ng-container>
<ng-template #etaSkeleton> <ng-template #etaSkeleton>
<span class="skeleton-loader"></span> <span class="skeleton-loader" style="max-width: 200px;"></span>
</ng-template> </ng-template>
</div> </div>
</div> </div>
@@ -118,7 +118,7 @@
</div> </div>
<span class="explainer">&nbsp;</span> <span class="explainer">&nbsp;</span>
} @else { } @else {
@if (!tx.status?.confirmed && showAccelerationSummary) { @if (tx && !tx.status?.confirmed && showAccelerationSummary) {
<ng-container *ngIf="(ETA$ | async) as eta;"> <ng-container *ngIf="(ETA$ | async) as eta;">
<app-accelerate-checkout <app-accelerate-checkout
*ngIf="(da$ | async) as da;" *ngIf="(da$ | async) as da;"
@@ -135,7 +135,7 @@
></app-accelerate-checkout> ></app-accelerate-checkout>
</ng-container> </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) { @if (tx?.acceleration && !tx.status?.confirmed) {
<div class="progress-icon"> <div class="progress-icon">
<fa-icon [icon]="['fas', 'wand-magic-sparkles']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'wand-magic-sparkles']" [fixedWidth]="true"></fa-icon>
@@ -185,7 +185,11 @@
} }
</div> </div>
<div class="footer-link" [routerLink]="['/tx' | relativeUrl, tx?.txid]"> <div class="footer-link"
[routerLink]="['/tx' | relativeUrl, tx?.txid || txId]"
[queryParams]="{ mode: 'details' }"
queryParamsHandling="merge"
>
<span><ng-container i18n="accelerator.show-more-details">See more details</ng-container>&nbsp;<fa-icon [icon]="['fas', 'arrow-alt-circle-right']"></fa-icon></span> <span><ng-container i18n="accelerator.show-more-details">See more details</ng-container>&nbsp;<fa-icon [icon]="['fas', 'arrow-alt-circle-right']"></fa-icon></span>
</div> </div>
</div> </div>
@@ -49,7 +49,7 @@
position: relative; position: relative;
background: var(--nav-bg); background: var(--nav-bg);
box-shadow: 0 -5px 15px #000; box-shadow: 0 -5px 15px #000;
z-index: 100; z-index: 99;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -31,6 +31,8 @@ import { TrackerStage } from './tracker-bar.component';
import { MiningService, MiningStats } from '../../services/mining.service'; import { MiningService, MiningStats } from '../../services/mining.service';
import { ETA, EtaService } from '../../services/eta.service'; import { ETA, EtaService } from '../../services/eta.service';
import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils'; import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
interface Pool { interface Pool {
id: number; id: number;
@@ -140,6 +142,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
private priceService: PriceService, private priceService: PriceService,
private enterpriseService: EnterpriseService, private enterpriseService: EnterpriseService,
private miningService: MiningService, private miningService: MiningService,
private router: Router,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private zone: NgZone, private zone: NgZone,
@Inject(ZONE_SERVICE) private zoneService: any, @Inject(ZONE_SERVICE) private zoneService: any,
@@ -62,6 +62,8 @@
</div> </div>
} }
<span id="accelerate"></span>
<ng-template [ngIf]="!isLoadingTx && !error"> <ng-template [ngIf]="!isLoadingTx && !error">
<!-- CPFP Details --> <!-- CPFP Details -->
@@ -167,7 +169,7 @@
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2> <h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline> <app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
<br> <br>
</ng-container> </ng-container>
@@ -550,19 +552,19 @@
<td> <td>
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton"> <ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
@if (eta.blocks >= 7) { @if (eta.blocks >= 7) {
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''"> <span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span> <span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
<a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> <a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
} }
</span> </span>
} @else if (network === 'liquid' || network === 'liquidtestnet') { } @else if (network === 'liquid' || network === 'liquidtestnet') {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> <app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
} @else { } @else {
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''"> <span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> <app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
<a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> <a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
} }
</span> </span>
<span class="eta justify-content-end"> <span class="eta justify-content-end">
@@ -604,7 +606,17 @@
@if (!isLoadingTx) { @if (!isLoadingTx) {
<tr> <tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td> <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) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo.bidBoost | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + accelerationInfo.bidBoost"></app-fiat></span>
} @else if (tx.feeDelta && !accelerationInfo) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + tx.feeDelta"></app-fiat></span>
} @else {
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span>
}
</td>
</tr> </tr>
} @else { } @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container> <ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
@@ -640,9 +652,9 @@
<td> <td>
<div class="effective-fee-container"> <div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) { @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 { } @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) { @if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) {
@@ -297,7 +297,6 @@
} }
.etaDeepMempool { .etaDeepMempool {
display: flex !important;
justify-content: flex-end; justify-content: flex-end;
flex-wrap: wrap; flex-wrap: wrap;
align-content: center; align-content: center;
@@ -333,3 +332,7 @@
top: -1px; top: -1px;
margin-right: 2px; margin-right: 2px;
} }
.oobFees {
color: #905cf4;
}
@@ -11,7 +11,9 @@ import {
tap, tap,
map, map,
retry, retry,
startWith startWith,
repeat,
take
} from 'rxjs/operators'; } from 'rxjs/operators';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject } from 'rxjs'; import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject } from 'rxjs';
@@ -76,6 +78,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
transactionTime = -1; transactionTime = -1;
subscription: Subscription; subscription: Subscription;
fetchCpfpSubscription: Subscription; fetchCpfpSubscription: Subscription;
transactionTimesSubscription: Subscription;
fetchRbfSubscription: Subscription; fetchRbfSubscription: Subscription;
fetchCachedTxSubscription: Subscription; fetchCachedTxSubscription: Subscription;
fetchAccelerationSubscription: Subscription; fetchAccelerationSubscription: Subscription;
@@ -107,6 +110,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
showCpfpDetails = false; showCpfpDetails = false;
miningStats: MiningStats; miningStats: MiningStats;
fetchCpfp$ = new Subject<string>(); fetchCpfp$ = new Subject<string>();
transactionTimes$ = new Subject<string>();
fetchRbfHistory$ = new Subject<string>(); fetchRbfHistory$ = new Subject<string>();
fetchCachedTx$ = new Subject<string>(); fetchCachedTx$ = new Subject<string>();
fetchAcceleration$ = new Subject<number>(); fetchAcceleration$ = new Subject<number>();
@@ -226,6 +230,25 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.latestBlock = blocks[0]; this.latestBlock = blocks[0];
}); });
this.transactionTimesSubscription = this.transactionTimes$.pipe(
tap(() => {
this.isLoadingFirstSeen = true;
}),
switchMap((txid) => this.apiService.getTransactionTimes$([txid]).pipe(
retry({ count: 2, delay: 2000 }),
// Try again until we either get a valid response, or the transaction is confirmed
repeat({ delay: 2000 }),
filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed),
take(1),
)),
)
.subscribe((transactionTimes) => {
this.isLoadingFirstSeen = false;
if (transactionTimes?.length && transactionTimes[0]) {
this.transactionTime = transactionTimes[0];
}
});
this.fetchCpfpSubscription = this.fetchCpfp$ this.fetchCpfpSubscription = this.fetchCpfp$
.pipe( .pipe(
switchMap((txId) => switchMap((txId) =>
@@ -388,10 +411,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
const isConflict = audit.fullrbfTxs.includes(txid); const isConflict = audit.fullrbfTxs.includes(txid);
const isExpected = audit.template.some(tx => tx.txid === txid); const isExpected = audit.template.some(tx => tx.txid === txid);
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time; const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
const wasSeen = audit.version === 1 ? !audit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
return { return {
seen: isExpected || isPrioritized || isAccelerated, seen: wasSeen,
expected: isExpected, expected: isExpected,
added: isAdded, added: isAdded && (audit.version === 0 || !wasSeen),
prioritized: isPrioritized, prioritized: isPrioritized,
conflict: isConflict, conflict: isConflict,
accelerated: isAccelerated, accelerated: isAccelerated,
@@ -438,9 +462,23 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (txPosition.position.acceleratedBy) { if (txPosition.position.acceleratedBy) {
txPosition.cpfp.acceleratedBy = 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); this.setCpfpInfo(txPosition.cpfp);
} else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) { } else if ((this.tx?.acceleration)) {
this.tx.acceleratedBy = txPosition.position.acceleratedBy; 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 === '') { if (this.stateService.network === '') {
@@ -495,6 +533,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}); });
} }
} }
if (window.innerWidth <= 767.98) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
queryParamsHandling: 'merge',
preserveFragment: true,
queryParams: { mode: 'details' },
replaceUrl: true,
});
}
this.seoService.setTitle( this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:` $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
); );
@@ -573,7 +619,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (tx.firstSeen) { if (tx.firstSeen) {
this.transactionTime = tx.firstSeen; this.transactionTime = tx.firstSeen;
} else { } else {
this.getTransactionTime(); this.transactionTimes$.next(tx.txid);
} }
} else { } else {
this.fetchAcceleration$.next(tx.status.block_height); this.fetchAcceleration$.next(tx.status.block_height);
@@ -599,8 +645,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
ancestors: tx.ancestors, ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant, bestDescendant: tx.bestDescendant,
}); });
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant); const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant || tx.descendants);
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) >= 0.1)); this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && tx.effectiveFeePerVsize !== (this.tx.fee / (this.tx.weight / 4)) && tx.effectiveFeePerVsize !== (tx.fee / Math.ceil(tx.weight / 4)));
} else { } else {
this.fetchCpfp$.next(this.tx.txid); this.fetchCpfp$.next(this.tx.txid);
} }
@@ -730,7 +776,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.accelerationPositions, this.accelerationPositions,
); );
}) })
) );
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@@ -764,28 +810,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return of(false); return of(false);
} }
getTransactionTime() {
this.isLoadingFirstSeen = true;
this.apiService
.getTransactionTimes$([this.tx.txid])
.pipe(
retry({ count: 2, delay: 2000 }),
catchError(() => {
this.isLoadingFirstSeen = false;
return throwError(() => new Error(''));
})
)
.subscribe((transactionTimes) => {
if (transactionTimes?.length && transactionTimes[0]) {
this.transactionTime = transactionTimes[0];
} else {
setTimeout(() => {
this.getTransactionTime();
}, 2000);
}
});
}
setCpfpInfo(cpfpInfo: CpfpInfo): void { setCpfpInfo(cpfpInfo: CpfpInfo): void {
if (!cpfpInfo || !this.tx) { if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null; this.cpfpInfo = null;
@@ -815,6 +839,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.acceleration = cpfpInfo.acceleration; this.tx.acceleration = cpfpInfo.acceleration;
this.tx.acceleratedBy = cpfpInfo.acceleratedBy; this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.tx.acceleratedAt = cpfpInfo.acceleratedAt; this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
this.tx.feeDelta = cpfpInfo.feeDelta;
this.setIsAccelerated(firstCpfp); this.setIsAccelerated(firstCpfp);
} }
@@ -829,8 +854,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.sigops = this.cpfpInfo.sigops; this.sigops = this.cpfpInfo.sigops;
this.adjustedVsize = this.cpfpInfo.adjustedVsize; this.adjustedVsize = this.cpfpInfo.adjustedVsize;
} }
this.hasCpfp =!!(this.cpfpInfo && (this.cpfpInfo.bestDescendant || this.cpfpInfo.descendants?.length || this.cpfpInfo.ancestors?.length)); this.hasCpfp =!!(this.cpfpInfo && relatives.length);
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01)); this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && this.tx.effectiveFeePerVsize !== (this.tx.fee / (this.tx.weight / 4)) && this.tx.effectiveFeePerVsize !== (this.tx.fee / Math.ceil(this.tx.weight / 4)));
} }
setIsAccelerated(initialState: boolean = false) { setIsAccelerated(initialState: boolean = false) {
@@ -842,7 +867,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} }
if (this.isAcceleration) { if (this.isAcceleration) {
// this immediately returns cached stats if we fetched them recently // this immediately returns cached stats if we fetched them recently
this.miningService.getMiningStats('1w').subscribe(stats => { this.miningService.getMiningStats('1m').subscribe(stats => {
this.miningStats = stats; this.miningStats = stats;
this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
}); });
@@ -1058,6 +1083,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnDestroy() { ngOnDestroy() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
this.fetchCpfpSubscription.unsubscribe(); this.fetchCpfpSubscription.unsubscribe();
this.transactionTimesSubscription.unsubscribe();
this.fetchRbfSubscription.unsubscribe(); this.fetchRbfSubscription.unsubscribe();
this.fetchCachedTxSubscription.unsubscribe(); this.fetchCachedTxSubscription.unsubscribe();
this.fetchAccelerationSubscription.unsubscribe(); this.fetchAccelerationSubscription.unsubscribe();
@@ -43,7 +43,7 @@
<span *ngSwitchCase="'output'" i18n="transaction.output">Output</span> <span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
<span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span> <span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span>
</ng-container> </ng-container>
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span> <span *ngIf="line.type !== 'fee'"> #{{ line.index }}</span>
<ng-container [ngSwitch]="line.type"> <ng-container [ngSwitch]="line.type">
<span *ngSwitchCase="'input'"> <span *ngSwitchCase="'input'">
<ng-container *ngIf="line.status?.block_height"> <ng-container *ngIf="line.status?.block_height">
@@ -73,7 +73,7 @@
<app-truncate [text]="line.txid"></app-truncate> <app-truncate [text]="line.txid"></app-truncate>
</p> </p>
<ng-container [ngSwitch]="line.type"> <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.status?.block_height">
<ng-container *ngIf="line.blockHeight; else noBlockHeight"> <ng-container *ngIf="line.blockHeight; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: line.blockHeight - line?.status?.block_height, connector: true}"></ng-container> <ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: line.blockHeight - line?.status?.block_height, connector: true}"></ng-container>
@@ -83,7 +83,7 @@
</ng-template> </ng-template>
</ng-container> </ng-container>
</p> </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.blockHeight">
<ng-container *ngIf="line?.status?.block_height; else noBlockHeight"> <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> <ng-container *ngTemplateOutlet="nBlocksLater; context:{n: line?.status?.block_height - line.blockHeight, connector: true}"></ng-container>
@@ -17,11 +17,13 @@ export interface Transaction {
feePerVsize?: number; feePerVsize?: number;
effectiveFeePerVsize?: number; effectiveFeePerVsize?: number;
ancestors?: Ancestor[]; ancestors?: Ancestor[];
descendants?: Ancestor[];
bestDescendant?: BestDescendant | null; bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean; cpfpChecked?: boolean;
acceleration?: boolean; acceleration?: boolean;
acceleratedBy?: number[]; acceleratedBy?: number[];
acceleratedAt?: number; acceleratedAt?: number;
feeDelta?: number;
deleteAfter?: number; deleteAfter?: number;
_unblinded?: any; _unblinded?: any;
_deduced?: boolean; _deduced?: boolean;
@@ -31,6 +31,7 @@ export interface CpfpInfo {
acceleration?: boolean; acceleration?: boolean;
acceleratedBy?: number[]; acceleratedBy?: number[];
acceleratedAt?: number; acceleratedAt?: number;
feeDelta?: number;
} }
export interface RbfInfo { export interface RbfInfo {
@@ -210,6 +211,8 @@ export interface BlockExtended extends Block {
} }
export interface BlockAudit extends BlockExtended { export interface BlockAudit extends BlockExtended {
version: number,
unseenTxs?: string[],
missingTxs: string[], missingTxs: string[],
addedTxs: string[], addedTxs: string[],
prioritizedTxs: string[], prioritizedTxs: string[],
@@ -236,7 +239,7 @@ export interface TransactionStripped {
acc?: boolean; acc?: boolean;
flags?: number | null; flags?: number | null;
time?: number; time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }
@@ -250,6 +253,8 @@ export interface MempoolPosition {
vsize: number, vsize: number,
accelerated?: boolean, accelerated?: boolean,
acceleratedBy?: number[], acceleratedBy?: number[],
acceleratedAt?: number,
feeDelta?: number,
} }
export interface AccelerationPosition extends MempoolPosition { export interface AccelerationPosition extends MempoolPosition {
@@ -408,6 +413,7 @@ export interface Acceleration {
bidBoost?: number; bidBoost?: number;
boostCost?: number; boostCost?: number;
boostRate?: number; boostRate?: number;
minedByPoolUniqueId?: number;
} }
export interface AccelerationHistoryParams { export interface AccelerationHistoryParams {
@@ -7,7 +7,7 @@ export interface WebsocketResponse {
backend?: 'esplora' | 'electrum' | 'none'; backend?: 'esplora' | 'electrum' | 'none';
block?: BlockExtended; block?: BlockExtended;
blocks?: BlockExtended[]; blocks?: BlockExtended[];
conversions?: any; conversions?: Record<string, number>;
txConfirmed?: string; txConfirmed?: string;
historicalDate?: string; historicalDate?: string;
mempoolInfo?: MempoolInfo; mempoolInfo?: MempoolInfo;
@@ -68,7 +68,7 @@ h3 {
.loading-spinner { .loading-spinner {
position: absolute; position: absolute;
top: 400px; top: 400px;
z-index: 100; z-index: 99;
width: 100%; width: 100%;
left: 0; left: 0;
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
@@ -16,7 +16,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
}) })
@@ -19,7 +19,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
}) })
@@ -102,7 +102,7 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
top: 550px; top: 550px;
} }
@@ -110,7 +110,7 @@
.loading-spinner.widget { .loading-spinner.widget {
position: absolute; position: absolute;
top: 200px; top: 200px;
z-index: 100; z-index: 99;
width: 100%; width: 100%;
left: 0; left: 0;
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
@@ -120,7 +120,7 @@
.loading-spinner.nodepage { .loading-spinner.nodepage {
position: absolute; position: absolute;
top: 200px; top: 200px;
z-index: 100; z-index: 99;
width: 100%; width: 100%;
left: 0; left: 0;
} }
@@ -128,7 +128,7 @@
.loading-spinner.channelpage { .loading-spinner.channelpage {
position: absolute; position: absolute;
top: 400px; top: 400px;
z-index: 100; z-index: 99;
width: 100%; width: 100%;
left: 0; left: 0;
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
@@ -8,7 +8,7 @@
left: 0; left: 0;
right: 0; right: 0;
width: 100%; width: 100%;
z-index: 100; z-index: 99;
} }
.spinner-border { .spinner-border {
@@ -68,7 +68,7 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
top: 550px; top: 550px;
} }
@@ -22,7 +22,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -46,7 +46,7 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
.pool-distribution { .pool-distribution {
@@ -22,7 +22,7 @@ import { StateService } from '../../services/state.service';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 99;
} }
`], `],
}) })
+22
View File
@@ -0,0 +1,22 @@
import { Injectable, inject } from '@angular/core';
import { CanMatchFn, Route, Router, UrlSegment } from '@angular/router';
import { NavigationService } from './services/navigation.service';
@Injectable({
providedIn: 'root'
})
class GuardService {
constructor(
private router: Router,
private navigationService: NavigationService,
) {}
trackerGuard(route: Route, segments: UrlSegment[]): boolean {
const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98;
}
}
export const TrackerGuard: CanMatchFn = (route: Route, segments: UrlSegment[]): boolean => {
return inject(GuardService).trackerGuard(route, segments);
};
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff. Show more