Compare commits

..

227 Commits

Author SHA1 Message Date
wiz
617789926b Release v3.0.1
- No code change from v3.0.0, just enables RUST_GBT by default
2024-10-02 04:01:52 +09:00
wiz
001c596d14 Enable RUST_GBT in backend by default 2024-10-02 04:00:51 +09:00
wiz
f0af1703da Release v3.0.0 2024-08-24 18:35:41 +09:00
softsimon
5452d7f524 pull from transifex 2024-08-20 09:32:56 +02:00
wiz
ff9e2456b9 ops: Tweak build script to support tags 2024-08-20 15:22:16 +09:00
wiz
4e581347c8 Bump version to v3.0.0-rc1 2024-08-20 12:06:11 +09:00
wiz
820777236e Merge pull request #5465 from mempool/orangesurf/update-logos-2024-08-19
Add logo images and references to logos
2024-08-20 12:01:51 +09:00
wiz
beeb5eb08c Merge pull request #5466 from mempool/mononaut/fix-about-layout
Fix about page layout
2024-08-20 10:50:04 +09:00
Mononaut
b78aca0282 Fix about page layout 2024-08-19 19:46:22 +00:00
orangesurf
9572f2d554 Add logo images and references to logos 2024-08-19 20:13:49 +02:00
softsimon
ef13596b59 Merge pull request #5449 from mempool/mononaut/non-acc-effective-fee
fix mined acceleration detection logic on tx pages
2024-08-19 17:10:46 +02:00
wiz
80da024bbb Add hr locale to angular.json 2024-08-19 14:49:36 +09:00
natsoni
f75f85f914 Hide fee delta on accelerated tx mined by participating pool with 0 bid boost 2024-08-18 19:43:38 +02:00
natsoni
b3ac107b0b clear feeDelta if a tx is mined by non-participating pool 2024-08-18 18:35:30 +02:00
softsimon
f8cedaa7a3 Merge pull request #5462 from mempool/natsoni/fix-console-error
Fix accelerated arrow not appearing
2024-08-18 15:47:02 +02:00
natsoni
72bb92dd8b Merge branch 'master' into mononaut/non-acc-effective-fee 2024-08-18 14:27:48 +02:00
natsoni
e3c4e219f3 Fix accelerated arrow not appearing 2024-08-18 14:15:56 +02:00
wiz
aa3fa4478a Merge pull request #5458 from mempool/mononaut/pool-pie-colors
update pool pie chart color scheme
2024-08-18 12:15:27 +09:00
Mononaut
26c03eee88 update pool pie chart color scheme 2024-08-14 14:21:47 +00:00
softsimon
db10ab9aae pull from transifex 2024-08-13 10:28:42 +02:00
wiz
2ee7b9531a Merge pull request #5454 from mempool/simon/add-croatian
Add Croatian language
2024-08-13 13:36:00 +09:00
wiz
5f6af83944 Merge pull request #5453 from mempool/mononaut/acceleration-sparkles
acceleration sparkles
2024-08-13 13:33:25 +09:00
softsimon
8d2204a53f Merge pull request #5457 from mempool/mononaut/flow-output-indices
flow diagram zero-indexed inputs & outputs
2024-08-12 23:04:34 +02:00
Mononaut
96bec279a9 flow diagram zero-indexed inputs & outputs 2024-08-12 14:54:51 +00:00
softsimon
5178ae43f6 Add Croatian language 2024-08-12 00:07:48 +02:00
softsimon
ca26154426 pull from transifex 2024-08-11 23:51:16 +02:00
Mononaut
021f0b32a1 sparklier sparkles 2024-08-11 20:52:26 +00:00
Mononaut
b8cfeb579b make accelerations magical again 2024-08-11 20:38:54 +00:00
softsimon
fc5b99f93f Merge pull request #5452 from mempool/mononaut/tx-v1-audit
Implement v1 audit in tx audit API
2024-08-11 00:18:09 +02:00
Mononaut
ce4b0ed0f3 Implement v1 audit in tx audit API 2024-08-10 21:57:31 +00:00
Mononaut
a31729b8b8 fix feeDelta display logic 2024-08-10 21:56:11 +00:00
Mononaut
79e494150c fix mined acceleration detection logic on tx pages 2024-08-09 14:44:51 +00:00
softsimon
b1a43abc0e Merge pull request #5444 from mempool/natsoni/fix-pool-pie-position
Fix pool pie position on safari
2024-08-08 22:20:09 +02:00
softsimon
3e50a3c9e7 pull from transifex 2024-08-08 18:58:59 +02:00
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
166 changed files with 17482 additions and 9914 deletions

12
LICENSE
View File

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

View File

@@ -28,7 +28,7 @@
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"AUDIT": false,
"RUST_GBT": false,
"RUST_GBT": true,
"LIMIT_GBT": false,
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6,

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -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
class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
: { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
}
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
const unseen: string[] = []; // present in the mined block, not in our mempool
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 rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse
const isDisplaced = {};
const isAccelerated = {};
let displacedWeight = 0;
let matchedWeight = 0;
let projectedWeight = 0;
@@ -32,6 +34,7 @@ class Audit {
inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid);
isAccelerated[tx.txid] = true;
}
}
// coinbase is always expected
@@ -113,11 +116,16 @@ class Audit {
} else {
if (rbfCache.has(tx.txid)) {
rbf.push(tx.txid);
} else if (!isDisplaced[tx.txid]) {
if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) {
unseen.push(tx.txid);
}
} else {
if (mempool[tx.txid]) {
prioritized.push(tx.txid);
if (isDisplaced[tx.txid]) {
added.push(tx.txid);
}
} else {
added.push(tx.txid);
unseen.push(tx.txid);
}
}
overflowWeight += tx.weight;
@@ -125,6 +133,24 @@ class Audit {
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
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
let maxOverflowRate = 0;
@@ -165,6 +191,7 @@ class Audit {
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return {
unseen,
censored: Object.keys(isCensored),
added,
prioritized,

View File

@@ -165,6 +165,7 @@ class BitcoinRoutes {
acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined,
feeDelta: tx.feeDelta || undefined,
});
return;
}

View File

@@ -33,6 +33,7 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -439,7 +440,7 @@ class Blocks {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
if (cpfpSummary) {
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
@@ -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 blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
@@ -927,12 +933,12 @@ class Blocks {
const newBlock = await this.$indexBlock(lastBlock.height - i);
this.blocks.push(newBlock);
this.updateTimerProgress(timer, `reindexed block`);
let cpfpSummary;
let newCpfpSummary;
if (config.MEMPOOL.CPFP_INDEXING) {
cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
this.updateTimerProgress(timer, `reindexed block cpfp`);
}
await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height);
this.updateTimerProgress(timer, `reindexed block summary`);
}
await mining.$indexDifficultyAdjustments();
@@ -981,7 +987,7 @@ class Blocks {
// start async callbacks
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions));
if (block.height % 2016 === 0) {
if (Common.indexingEnabled()) {
@@ -1178,7 +1184,7 @@ class Blocks {
};
}),
};
summaryVersion = 1;
summaryVersion = cpfpSummary.version;
} else {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
@@ -1397,11 +1403,11 @@ class Blocks {
return this.currentBlockHeight;
}
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise<CpfpSummary | null> {
let transactions = txs;
if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') {
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
}
if (!transactions) {
const block = await bitcoinClient.getBlock(hash, 2);
@@ -1413,7 +1419,7 @@ class Blocks {
}
if (transactions?.length != null) {
const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]);
const summary = calculateFastBlockCpfp(height, transactions);
await this.$saveCpfp(hash, height, summary);

View File

@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { Request } from 'express';
import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';

View File

@@ -6,7 +6,7 @@ import { Acceleration } from './acceleration/acceleration';
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
const MAX_CLUSTER_ITERATIONS = 100;
export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
@@ -93,6 +93,7 @@ export function calculateFastBlockCpfp(height: number, transactions: Transaction
return {
transactions,
clusters,
version: 1,
};
}
@@ -159,6 +160,7 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
return {
transactions: transactions.map(tx => txMap[tx.txid]),
clusters: clusterArray,
version: 2,
};
}

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 80;
private static currentVersion = 81;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -653,9 +653,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
if (isBitcoin === true) {
await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
}
await this.updateToSchemaVersion(75);
}
@@ -691,6 +693,13 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
await this.updateToSchemaVersion(80);
}
if (databaseSchemaVersion < 81 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81);
}
}
/**

View File

@@ -453,6 +453,7 @@ class MempoolBlocks {
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
@@ -460,6 +461,7 @@ class MempoolBlocks {
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {

View File

@@ -337,7 +337,7 @@ export function makeBlockTemplate(candidates: MempoolTransactionExtended[], acce
let failures = 0;
while (mempoolArray.length || modified.length) {
// skip invalid transactions
while (mempoolArray[0].used || mempoolArray[0].modified) {
while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
mempoolArray.shift();
}

View File

@@ -465,16 +465,12 @@ class MiningRoutes {
}
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('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
res.setHeader('expires', -1);
try {
accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send('ok');
res.status(200).send();
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}

View File

@@ -37,6 +37,7 @@ export interface AccelerationHistory {
};
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 _accelerations: Acceleration[] | null = null;
private lastPoll = 0;
@@ -52,7 +53,9 @@ class AccelerationApi {
}
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 {
@@ -70,7 +73,7 @@ class AccelerationApi {
}
public async $updateAccelerations(): Promise<Acceleration[] | null> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations();
if (accelerations) {
this._accelerations = accelerations;

View File

@@ -3,7 +3,7 @@ import * as WebSocket from 'ws';
import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
MempoolDelta, MempoolDeltaTxids
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -823,6 +823,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
};
@@ -864,6 +865,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
if (!mempoolTx.cpfpChecked) {
calculateMempoolTxCpfp(mempoolTx, newMempool);
@@ -931,6 +933,8 @@ class WebsocketHandler {
throw new Error('No WebSocket.Server have been set');
}
const blockTransactions = structuredClone(transactions);
this.printLogs();
await statistics.runStatistics();
@@ -940,7 +944,7 @@ class WebsocketHandler {
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
const accelerations = Object.values(mempool.getAccelerations());
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions);
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions);
@@ -960,7 +964,7 @@ class WebsocketHandler {
}
if (Common.indexingEnabled()) {
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@@ -982,9 +986,11 @@ class WebsocketHandler {
});
BlocksAuditsRepository.$saveAudit({
version: 1,
time: block.timestamp,
height: block.height,
hash: block.id,
unseenTxs: unseen,
addedTxs: added,
prioritizedTxs: prioritized,
missingTxs: censored,
@@ -1138,6 +1144,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
});
@@ -1160,6 +1167,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
}
}

View File

@@ -193,7 +193,7 @@ const defaults: IConfig = {
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
'AUDIT': false,
'RUST_GBT': false,
'RUST_GBT': true,
'LIMIT_GBT': false,
'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0,

View File

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

View File

@@ -29,9 +29,11 @@ export interface PoolStats extends PoolInfo {
}
export interface BlockAudit {
version: number,
time: number,
height: number,
hash: string,
unseenTxs: string[],
missingTxs: string[],
freshTxs: string[],
sigopTxs: string[],
@@ -126,6 +128,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
feeDelta?: number;
replacement?: boolean;
uid?: number;
flags?: number;
@@ -382,8 +385,9 @@ export interface CpfpCluster {
}
export interface CpfpSummary {
transactions: TransactionExtended[];
transactions: MempoolTransactionExtended[];
clusters: CpfpCluster[];
version: number;
}
export interface Statistic {
@@ -449,7 +453,7 @@ export interface OptimizedStatistic {
export interface TxTrackingInfo {
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?: {
ancestors?: Ancestor[],
bestDescendant?: Ancestor | null,
@@ -462,6 +466,7 @@ export interface TxTrackingInfo {
accelerated?: boolean,
acceleratedBy?: number[],
acceleratedAt?: number,
feeDelta?: number,
confirmed?: boolean
}

View File

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

View File

@@ -192,6 +192,7 @@ class AccelerationRepository {
}
}
// modifies block transactions
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
for (const tx of transactions) {

View File

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

View File

@@ -30,7 +30,7 @@ __MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true}
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
@@ -144,7 +144,7 @@ __REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=14819
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS

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}
__ACCELERATOR__=${ACCELERATOR:=false}
__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}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}

View File

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

View File

@@ -543,16 +543,7 @@ describe('Mainnet', () => {
}
});
cy.get('.alert').should('be.visible');
cy.get('.alert').invoke('css', 'width').then((alertWidth) => {
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
});
cy.get('.btn-warning').then(getRectangle).then((rectA) => {
cy.get('.alert').then(getRectangle).then((rectB) => {
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
});
});
cy.get('.alert-replaced').should('be.visible');
});
it('shows RBF transactions properly (desktop)', () => {

View File

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

View File

@@ -25,6 +25,7 @@
"HISTORICAL_PRICE": true,
"ADDITIONAL_CURRENCIES": false,
"ACCELERATOR": false,
"ACCELERATOR_BUTTON": true,
"PUBLIC_ACCELERATIONS": false,
"SERVICES_API": "https://mempool.space/api/v1/services"
}

View File

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

View File

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

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 { TrackerComponent } from './components/tracker/tracker.component';
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from './route-guards';
const browserWindow = window || {};
// @ts-ignore
@@ -140,16 +141,17 @@ let routes: Routes = [
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: 'tx',
canMatch: [TrackerGuard],
runGuardsAndResolvers: 'always',
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'tracker',
data: { networkSpecific: true },
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
},
{
path: 'wallet',
children: [],
@@ -213,10 +215,6 @@ let routes: Routes = [
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: ''
},
];
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),
data: { preload: true },
},
{
path: '**',
redirectTo: ''
},
];
}
if (!window['isMempoolSpaceBuild']) {
routes.push({
path: '**',
redirectTo: ''
});
}
@NgModule({
imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabledBlocking',

View File

@@ -151,7 +151,7 @@ export const languages: Language[] = [
{ code: 'fr', name: 'Français' }, // French
// { code: 'gl', name: 'Galego' }, // Galician
{ code: 'ko', name: '한국어' }, // Korean
// { code: 'hr', name: 'Hrvatski' }, // Croatian
{ code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese

View File

@@ -53,7 +53,7 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76" class="image">
<defs>
<style>
.d {
@@ -125,7 +125,9 @@
<span>Blockstream</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
</svg>
<span>Unchained</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
@@ -150,7 +152,7 @@
<span>Bull Bitcoin</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)">
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
@@ -435,7 +437,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@@ -13,8 +13,6 @@
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro {
@@ -158,9 +156,8 @@
margin: 40px 29px 10px;
&.image.coldcard {
border-radius: 0;
width: auto;
max-height: 50px;
margin: 40px 29px 14px 29px;
height: auto;
margin: 20px 29px 20px;
}
}
}

View File

@@ -389,16 +389,30 @@
</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">
<p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p>
</div>
}
}
@if (canPayWithCashapp) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
<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>
<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>
@@ -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>
</div>
</div>
} @else if (step === 'cashapp') {
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
<!-- 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">
<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>
@@ -437,7 +451,7 @@
</div>
</div>
@if (!loadingCashapp) {
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
@@ -456,8 +470,14 @@
<div class="row text-center mt-1">
<div class="col-sm">
<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 (loadingCashapp) {
@if (step === 'applepay') {
<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">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
@@ -505,7 +525,7 @@
<div class="col-sm">
<div class="d-flex flex-row flex-column justify-content-center align-items-center">
<span i18n="accelerator.confirming-acceleration-with-miners">Confirming your acceleration with our mining pool partners...</span>
@if (timeSincePaid > 20000) {
@if (timeSincePaid > 30000) {
<span i18n="accelerator.confirming-acceleration-with-miners">...sorry, this is taking longer than expected...</span>
}
<div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
@@ -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>
</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>
<div class="position-relative">

View File

@@ -11,8 +11,7 @@
.paymentMethod {
padding: 10px;
background-color: var(--secondary);
border-radius: 15px;
border: 2px solid var(--bg);
border-radius: 10px;
cursor: pointer;
}
@@ -202,4 +201,19 @@
.btn-error-wrapper {
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;
}

View File

@@ -1,7 +1,8 @@
/* eslint-disable no-console */
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
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 { AudioService } from '../../services/audio.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 { EnterpriseService } from '../../services/enterprise.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 = {
hasAccess: boolean;
@@ -24,7 +26,7 @@ export type AccelerationEstimate = {
mempoolBaseFee: number;
vsizeFee: number;
pools: number[];
availablePaymentMethods: {[method: string]: {min: number, max: number}};
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
unavailable?: boolean;
options: { // recommended bid options
fee: number; // recommended userBid in sats
@@ -47,7 +49,7 @@ export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
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({
selector: 'app-accelerate-checkout',
@@ -61,6 +63,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() eta: ETA;
@Input() scrollEvent: boolean;
@Input() cashappEnabled: boolean = true;
@Input() applePayEnabled: boolean = false;
@Input() googlePayEnabled: boolean = true;
@Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false;
@@ -79,17 +83,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
timePaid: number = 0; // time acceleration requested
math = Math;
isMobile: boolean = window.innerWidth <= 767.98;
isProdDomain = ['mempool.space',
'mempool-staging.va1.mempool.space',
'mempool-staging.fmt.mempool.space',
'mempool-staging.fra.mempool.space',
'mempool-staging.tk7.mempool.space',
'mempool-staging.sg1.mempool.space'
].indexOf(document.location.hostname) > -1;
private _step: CheckoutStep = 'summary';
simpleMode: boolean = true;
paymentMethod: 'cashapp' | 'btcpay';
timeoutTimer: any;
authSubscription$: Subscription;
auth: IAuth | null = null;
// accelerator stuff
square: { appId: string, locationId: string};
accelerationUUID: string;
accelerationSubscription: Subscription;
difficultySubscription: Subscription;
@@ -110,14 +119,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
// square
loadingCashapp = false;
cashappError = false;
cashappSubmit: any;
loadingApplePay = false;
loadingGooglePay = false;
payments: any;
cashAppPay: any;
cashAppSubscription: Subscription;
applePay: any;
googlePay: any;
conversionsSubscription: Subscription;
conversions: any;
conversions: Record<string, number>;
// btcpay
loadingBtcpayInvoice = false;
invoice = undefined;
@@ -133,9 +143,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private enterpriseService: EnterpriseService,
) {
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) => {
if (this.auth?.user?.userId !== auth?.user?.userId) {
this.auth = auth;
@@ -160,13 +176,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.moveToStep('summary');
}
this.servicesApiService.setupSquare$().subscribe(ids => {
this.square = {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
});
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@@ -174,7 +183,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
);
}
ngOnDestroy() {
ngOnDestroy(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
@@ -194,7 +203,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
}
moveToStep(step: CheckoutStep) {
moveToStep(step: CheckoutStep): void {
this._step = step;
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
@@ -203,6 +212,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.fetchEstimate();
}
if (this._step === 'checkout') {
this.insertSquare();
this.enterpriseService.goal(8);
}
if (this._step === 'checkout' && this.canPayWithBitcoin) {
@@ -212,15 +222,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.requestBTCPayInvoice();
} else if (this._step === 'cashapp' && this.cashappEnabled) {
this.loadingCashapp = true;
this.insertSquare();
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'applepay' && this.applePayEnabled) {
this.loadingApplePay = true;
this.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') {
this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => {
if (this.step === 'paid') {
this.accelerateError = 'internal_server_error';
}
}, 120000)
}, 120000);
}
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 {
setTimeout(() => {
this.scrollToElement(id, position);
}, timeout);
}
scrollToElement(id: string, position: ScrollLogicalPosition) {
scrollToElement(id: string, position: ScrollLogicalPosition): void {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
this.cd.markForCheck();
@@ -253,7 +271,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
/**
* Accelerator
*/
fetchEstimate() {
fetchEstimate(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
@@ -317,7 +335,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
}),
catchError((response) => {
catchError(() => {
this.estimate = undefined;
this.quoteError = `cannot_accelerate_tx`;
this.estimateSubscription.unsubscribe();
@@ -388,57 +406,248 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* Square
*/
insertSquare(): void {
//@ts-ignore
if (window.Square) {
if (!this.isProdDomain && !isDevMode()) {
return;
}
if (window['Square']) {
return;
}
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
if (document.location.hostname === 'mempool-staging.fmt.mempool.space' ||
document.location.hostname === 'mempool-staging.va1.mempool.space' ||
document.location.hostname === 'mempool-staging.fra.mempool.space' ||
document.location.hostname === 'mempool-staging.tk7.mempool.space' ||
document.location.hostname === 'mempool.space') {
statsUrl = 'https://web.squarecdn.com/v1/square.js';
if (this.isProdDomain) {
statsUrl = '/square/v1/square.js';
}
(function() {
(function(): void {
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);
})();
}
setupSquare() {
const init = () => {
setupSquare(): void {
if (!this.isProdDomain && !isDevMode()) {
return;
}
const init = (): void => {
this.initSquare();
};
//@ts-ignore
if (!window.Square) {
console.debug('Square.js failed to load properly. Retrying in 1 second.');
setTimeout(init, 1000);
if (!window['Square']) {
console.debug('Square.js failed to load properly. Retrying.');
setTimeout(this.setupSquare.bind(this), 100);
} else {
init();
}
}
async initSquare(): Promise<void> {
try {
//@ts-ignore
this.payments = window.Square.payments(this.square.appId, this.square.locationId)
await this.requestCashAppPayment();
this.servicesApiService.setupSquare$().subscribe({
next: async (ids) => {
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) {
console.debug('Error loading Square Payments', e);
this.cashappError = true;
return;
this.accelerateError = 'cannot_setup_square';
}
}
async requestCashAppPayment() {
if (this.cashAppSubscription) {
this.cashAppSubscription.unsubscribe();
}
/**
* APPLE PAY
*/
async requestApplePayPayment(): Promise<void> {
if (this.conversionsSubscription) {
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(
async (conversions) => {
this.conversions = conversions;
@@ -452,42 +661,37 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
countryCode: 'US',
currencyCode: 'USD',
total: {
amount: costUSD.toString(),
amount: costUSD.toFixed(2),
label: 'Total',
pending: true,
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`,
},
button: { shape: 'semiround', size: 'small', theme: 'light'}
productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
}
});
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`,
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
button: { shape: 'semiround', size: 'small', theme: 'light'}
redirectURL: `${redirectHostname}/tx/${this.tx.txid}`,
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
});
if (this.step === 'cashapp') {
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
}
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
this.loadingCashapp = false;
const that = this;
this.cashAppPay.addEventListener('ontokenization', function (event) {
this.cashAppPay.addEventListener('ontokenization', event => {
const { tokenResult, error } = event.detail;
if (error) {
this.accelerateError = error;
} else if (tokenResult.status === 'OK') {
that.servicesApiService.accelerateWithCashApp$(
that.tx.txid,
this.servicesApiService.accelerateWithCashApp$(
this.tx.txid,
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
that.accelerationUUID
this.accelerationUUID
).subscribe({
next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
that.audioService.playSound('ascend-chime-cartoon');
if (that.cashAppPay) {
that.cashAppPay.destroy();
this.audioService.playSound('ascend-chime-cartoon');
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
@@ -498,7 +702,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000);
},
error: (response) => {
that.accelerateError = response.error;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
@@ -517,7 +721,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
/**
* BTCPay
*/
async requestBTCPayInvoice() {
async requestBTCPayInvoice(): Promise<void> {
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
@@ -547,47 +751,61 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
/**
* UI events
*/
selectedOptionChanged(event) {
selectedOptionChanged(event): void {
this.selectedOption = event.target.id;
}
get step() {
get step(): CheckoutStep {
return this._step;
}
get paymentMethods() {
return Object.keys(this.estimate?.availablePaymentMethods || {});
get paymentMethods(): PaymentMethod[] {
return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[];
}
get couldPayWithBitcoin() {
get couldPayWithBitcoin(): boolean {
return !!this.estimate?.availablePaymentMethods?.bitcoin;
}
get couldPayWithCashapp() {
get couldPayWithCashapp(): boolean {
if (!this.cashappEnabled) {
return false;
}
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) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.balance;
}
get couldPay() {
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp;
get couldPay(): boolean {
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay || this.couldPayWithGooglePay;
}
get canPayWithBitcoin() {
get canPayWithBitcoin(): boolean {
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
}
get canPayWithCashapp() {
if (!this.cashappEnabled || !this.conversions) {
get canPayWithCashapp(): boolean {
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
@@ -598,11 +816,43 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return true;
}
}
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) {
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;
}
get canPay() {
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp;
get canPay(): boolean {
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay || this.canPayWithGooglePay;
}
get hasAccessToBalanceMode() {
get hasAccessToBalanceMode(): boolean {
return this.isLoggedIn() && this.estimate?.hasAccess;
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -26,7 +26,7 @@
<div class="node" [id]="'confirmed'">
<div class="acc-to-confirmed left go-faster"></div>
<div class="shape-border waiting">
<div class="shape animate"></div>
<div class="shape"></div>
</div>
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
</div>
@@ -58,7 +58,7 @@
<div class="nodes">
<div class="node" [id]="'first-seen'">
<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>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
@@ -80,7 +80,7 @@
} @else {
<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>
@if (!tx.status.confirmed) {
<div class="connector down loading"></div>
@@ -89,14 +89,14 @@
@if (tx.status.confirmed) {
<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) {
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
}
@if (useAbsoluteTime) {
<span>{{ acceleratedAt * 1000 | date }}</span>
} @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>
@@ -113,7 +113,10 @@
} @else {
<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>
@if (tx.status.confirmed) {
@@ -130,4 +133,10 @@
</div>
</div>
</div>
<app-acceleration-timeline-tooltip
[accelerationInfo]="hoverInfo"
[cursorPosition]="tooltipPosition"
></app-acceleration-timeline-tooltip>
</div>

View File

@@ -152,9 +152,16 @@
margin-bottom: -8px;
transform: translateY(-50%);
border-radius: 50%;
cursor: pointer;
padding: 4px;
background: transparent;
transition: background-color 300ms, padding 300ms;
&.hovering {
cursor: pointer;
&:hover {
padding: 0px;
}
}
.shape {
position: relative;

View File

@@ -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 { Transaction } from '../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
import { MiningService } from '../../services/mining.service';
@Component({
selector: 'app-acceleration-timeline',
@@ -10,6 +12,7 @@ import { Transaction } from '../../interfaces/electrs.interface';
export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;
@Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() eta: ETA;
// 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)
@@ -22,13 +25,25 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
useAbsoluteTime: boolean = false;
interval: number;
constructor() {}
tooltipPosition = null;
hoverInfo: any = null;
poolsData: { [id: number]: SinglePoolStats } = {};
constructor(
private miningService: MiningService,
) {}
ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? 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.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.poolsData[pool.unique_id] = pool;
}
});
this.interval = window.setInterval(() => {
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
@@ -52,4 +67,42 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
ngOnDestroy(): void {
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 };
}
}

View File

@@ -23,7 +23,7 @@ import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -4,8 +4,8 @@
<div class="clearfix"></div>
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
<div class="acceleration-list">
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
<ng-container *ngIf="pending">
@@ -16,11 +16,12 @@
<ng-container *ngIf="!pending">
<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="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="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
</ng-container>
</thead>
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of accelerations; let i= index;">
<td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
@@ -49,10 +50,21 @@
<a *ngIf="acceleration.blockHeight" [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a>
<span *ngIf="!acceleration.blockHeight">~</span>
</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">
<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('failed')" class="badge badge-danger" i18n="accelerator.canceled">Failed <span *ngIf="acceleration.status === 'failed_provisional'">🔄</span></span>
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@@ -61,22 +73,47 @@
</tr>
</tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="fee text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="fee-delta text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="status text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
</tr>
</tbody>
@if (!pending) {
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 200px"></span>
</td>
<td class="fee text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="block text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="pool text-right" *ngIf="!this.widget">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="status text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
</tr>
</tbody>
} @else {
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="fee-rate text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="bid text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
<td class="time text-right">
<span class="skeleton-loader" style="max-width: 100px"></span>
</td>
</tr>
</tbody>
}
</ng-template>
</table>

View File

@@ -12,7 +12,7 @@
padding-bottom: 0px;
}
.container-xl.legacy {
max-width: 1140px;
max-width: 1200px;
}
.container-xl.widget-container {
min-height: 335px;
@@ -72,9 +72,25 @@ tr, td, th {
.block {
width: 15%;
@media (max-width: 900px) {
display: none;
}
}
.pool {
width: 15%;
@media (max-width: 700px) {
display: none;
}
.pool-logo {
width: 18px;
height: 18px;
position: relative;
top: -1px;
margin-right: 2px;
}
}
.status {

View File

@@ -1,11 +1,12 @@
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 { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
import { ServicesApiServices } from '../../../services/services-api.service';
import { SeoService } from '../../../services/seo.service';
import { ActivatedRoute, Router } from '@angular/router';
import { MiningService } from '../../../services/mining.service';
@Component({
selector: 'app-accelerations-list',
@@ -30,11 +31,14 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
keyNavigationSubscription: Subscription;
dir: 'rtl' | 'ltr' = 'ltr';
paramSubscription: Subscription;
pools: { [id: number]: SinglePoolStats } = {};
nonEmptyAccelerations: boolean = true;
constructor(
private servicesApiService: ServicesApiServices,
private websocketService: WebsocketService,
public stateService: StateService,
private miningService: MiningService,
private cd: ChangeDetectorRef,
private seoService: SeoService,
private route: ActivatedRoute,
@@ -47,6 +51,12 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.pools[pool.unique_id] = pool;
}
});
if (!this.widget) {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
@@ -106,6 +116,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
for (const acc of accelerations) {
acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost;
}
this.nonEmptyAccelerations = accelerations.length > 0;
if (this.widget) {
return of(accelerations.slice(0, 6));
} else {

View File

@@ -1,7 +1,7 @@
@if (chartOnly) {
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
} @else {
<table>
<table style="width: 100%;">
<tbody>
<tr>
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
@@ -11,9 +11,9 @@
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
<app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
</div>
</td>

View File

@@ -61,4 +61,8 @@
& > div, & > div > svg {
overflow: visible !important;
}
}
.oobFees {
color: #905cf4;
}

View File

@@ -67,12 +67,17 @@ export class ActiveAccelerationBox implements OnChanges {
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
// Find the first pool with at least 1% of the total network hashrate
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
data.push(getDataItem(
pool.lastEstimatedHashrate,
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)),
index >= firstSignificantPool
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
: 'white',
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true,
) as PieSeriesOption);

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
});
}
}
}

View File

@@ -30,7 +30,7 @@ const periodSeconds = {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -249,7 +249,7 @@
</ng-template>
<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>
</ng-template>
@@ -259,7 +259,7 @@
</ng-template>
<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>
</ng-template>

View File

@@ -23,7 +23,7 @@ import { ActivatedRoute, Router } from '@angular/router';
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service';
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -24,7 +24,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -1,6 +1,5 @@
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles&trade; tooltip" ngbTooltip="select filter categories to highlight matching transactions">
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges float-right" i18n-ngbTooltip="Mempool Goggles&trade; tooltip" ngbTooltip="select filter categories to highlight matching transactions">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
</a>
<div class="filter-bar">

View File

@@ -24,7 +24,6 @@
display: flex;
flex-direction: row;
align-items: center;
float: right;
&:hover, &:active {
text-decoration: none;

View File

@@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service';
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -2,10 +2,12 @@
<div class="graph-alignment" [class.grid-align]="!autofit" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
<div class="block-overview-graph">
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div>
@if (!disableSpinner) {
<div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable">
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div>
}
<app-block-overview-tooltip
[tx]="selectedTx || hoverTx"
[cursorPosition]="tooltipPosition"

View File

@@ -18,6 +18,7 @@ const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
};
@@ -25,6 +26,7 @@ const unmatchedContrastAuditColors = {
censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity),
missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity),
added: setOpacity(contrastAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity),
};

View File

@@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
flags: number;
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
scene?: BlockScene;

View File

@@ -71,6 +71,7 @@ export const defaultAuditColors = {
censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'),
added_prioritized: darken(desaturate(hexToColor('0099ff'), 0.15), 0.85),
prioritized: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
accelerated: hexToColor('8f5ff6'),
};
@@ -101,6 +102,7 @@ export const contrastAuditColors = {
censored: hexToColor('ffa8ff'),
missing: darken(desaturate(hexToColor('ffa8ff'), 0.3), 0.7),
added: hexToColor('00bb98'),
added_prioritized: darken(desaturate(hexToColor('00bb98'), 0.15), 0.85),
prioritized: darken(desaturate(hexToColor('00bb98'), 0.3), 0.7),
accelerated: hexToColor('8f5ff6'),
};
@@ -136,6 +138,8 @@ export function defaultColorFunction(
return auditColors.missing;
case 'added':
return auditColors.added;
case 'added_prioritized':
return auditColors.added_prioritized;
case 'prioritized':
return auditColors.prioritized;
case 'selected':

View File

@@ -51,7 +51,7 @@
<tr *ngIf="hasEffectiveRate && effectiveRate != null">
<td *ngIf="!this.acceleration" class="label" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td *ngIf="this.acceleration" class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
<td class="value">
<td class="value" [class.oobFees]="this.acceleration">
<app-fee-rate [fee]="effectiveRate"></app-fee-rate>
</td>
</tr>
@@ -75,6 +75,10 @@
<span *ngSwitchCase="'freshcpfp'" class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span>
<span *ngSwitchCase="'added'" class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span *ngSwitchCase="'prioritized'" class="badge badge-warning" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
<ng-container *ngSwitchCase="'added_prioritized'">
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
</ng-container>
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>

View File

@@ -27,6 +27,9 @@ th, td {
width: 70%;
text-align: end;
}
&.oobFees {
color: #905cf4;
}
}
.badge.badge-accelerated {

View File

@@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service';
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service';
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -521,6 +521,7 @@ export class BlockComponent implements OnInit, OnDestroy {
if (transactions && blockAudit) {
const inTemplate = {};
const inBlock = {};
const isUnseen = {};
const isAdded = {};
const isPrioritized = {};
const isCensored = {};
@@ -543,6 +544,9 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.unseenTxs || []) {
isUnseen[txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
@@ -592,18 +596,27 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = 'accelerated';
}
}
for (const [index, tx] of transactions.entries()) {
let anySeen = false;
for (let index = transactions.length - 1; index >= 0; index--) {
const tx = transactions[index];
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (isPrioritized[tx.txid]) {
tx.status = 'prioritized';
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
tx.status = 'added_prioritized';
} else {
tx.status = 'prioritized';
}
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
anySeen = true;
tx.status = 'found';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else if (isUnseen[tx.txid] && anySeen) {
tx.status = 'added';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;

View File

@@ -15,7 +15,7 @@ import { StateService } from '../../services/state.service';
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
})

View File

@@ -23,7 +23,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils';
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -59,7 +59,7 @@
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
.loadingGraphs.widget {
top: 75%;

View File

@@ -28,7 +28,7 @@ interface Hashrate {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -17,7 +17,7 @@ const OUTLIERS_MEDIAN_MULTIPLIER = 4;
position: absolute;
top: 50%;
left: calc(50% - 16px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -11,7 +11,7 @@ import { StateService } from '../../services/state.service';
position: absolute;
top: 50%;
left: calc(50% - 16px);
z-index: 100;
z-index: 99;
}
`],
templateUrl: './lbtc-pegs-graph.component.html',

View File

@@ -52,7 +52,7 @@
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet4'] || '/testnet4')" ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet4'] || '/testnet4')" ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>

View File

@@ -2,5 +2,5 @@
position: absolute;
top: 50%;
left: calc(50% - 16px);
z-index: 100;
z-index: 99;
}

View File

@@ -1,7 +1,7 @@
.sticky-loading {
position: absolute;
right: 10px;
z-index: 100;
z-index: 99;
font-size: 14px;
@media (width >= 992px) {
left: 32px;

View File

@@ -70,7 +70,7 @@
<a ngbDropdownItem *ngIf="env.MAINNET_ENABLED" class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet4" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet4" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
</div>

View File

@@ -51,7 +51,8 @@
</div>
</ng-template>
</div>
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + (blockWidth * 0.3) + containerOffset + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
<app-acceleration-sparkles [style]="{ position: 'absolute', right: 0}" [arrow]="arrowElement" [run]="acceleratingArrow"></app-acceleration-sparkles>
<div *ngIf="arrowVisible" #arrowUp id="arrow-up" [ngStyle]="{'right': rightPosition + (blockWidth * 0.3) + containerOffset + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
</div>
</ng-container>

View File

@@ -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 { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
@@ -77,6 +77,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
maxArrowPosition = 0;
rightPosition = 0;
transition = 'background 2s, right 2s, transform 1s';
@ViewChild('arrowUp')
arrowElement: ElementRef<HTMLDivElement>;
acceleratingArrow: boolean = false;
markIndex: number;
txPosition: MempoolPosition;
@@ -201,6 +204,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.markBlocksSubscription = this.stateService.markBlock$
.subscribe((state) => {
const oldTxPosition = this.txPosition;
this.markIndex = undefined;
this.txPosition = undefined;
this.txFeePerVSize = undefined;
@@ -209,6 +213,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
if (state.mempoolPosition) {
this.txPosition = state.mempoolPosition;
if (this.txPosition.accelerated && !oldTxPosition?.accelerated) {
this.acceleratingArrow = true;
setTimeout(() => {
this.acceleratingArrow = false;
}, 2000);
}
}
if (state.txFeePerVSize) {
this.txFeePerVSize = state.txFeePerVSize;

View File

@@ -18,7 +18,7 @@ import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/grap
position: absolute;
top: 50%;
left: calc(50% - 16px);
z-index: 100;
z-index: 99;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -60,7 +60,7 @@
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
.pool-distribution {

View File

@@ -167,7 +167,7 @@ div.scrollable {
.loadingGraphs {
position: absolute;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
top: 475px;
@media (max-width: 992px) {
top: 600px;

View File

@@ -5,7 +5,7 @@
<br><br>
<h2>Privacy Policy</h2>
<h6>Updated: July 10, 2024</h6>
<h6>Updated: July 31, 2024</h6>
<br><br>
@@ -27,7 +27,7 @@
<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>
@@ -35,7 +35,7 @@
<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>
@@ -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 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>
@@ -74,22 +74,38 @@
<br>
</ng-container>
<ng-container *ngIf="officialMempoolSpace">
<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>

View File

@@ -46,7 +46,7 @@
@if (replaced) {
<div class="alert-replaced" role="alert">
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
<app-truncate [text]="latestReplacement" [lastChars]="12" [link]="['/tracker/' | relativeUrl, latestReplacement]"></app-truncate>
<app-truncate [text]="latestReplacement" [lastChars]="12" [link]="['/tx/' | relativeUrl, latestReplacement]" [queryParams]="{mode: 'status'}"></app-truncate>
</div>
} @else {
<div class="tracker-bar">
@@ -61,7 +61,7 @@
@if (transactionTime > 0) {
<i><app-time kind="since" [time]="transactionTime" [fastRender]="true" [showTooltip]="true"></app-time></i>
} @else {
<span class="skeleton-loader" style="max-width: 50%;"></span>
<span class="skeleton-loader" style="max-width: 200px;"></span>
}
</div>
</div>
@@ -78,7 +78,7 @@
</span>
</ng-container>
<ng-template #etaSkeleton>
<span class="skeleton-loader"></span>
<span class="skeleton-loader" style="max-width: 200px;"></span>
</ng-template>
</div>
</div>
@@ -118,7 +118,7 @@
</div>
<span class="explainer">&nbsp;</span>
} @else {
@if (!tx.status?.confirmed && showAccelerationSummary) {
@if (tx && !tx.status?.confirmed && showAccelerationSummary) {
<ng-container *ngIf="(ETA$ | async) as eta;">
<app-accelerate-checkout
*ngIf="(da$ | async) as da;"
@@ -135,7 +135,7 @@
></app-accelerate-checkout>
</ng-container>
}
<div class="status-panel d-flex flex-column h-100 w-100 justify-content-center align-items-center" [class.small-status]="!tx.status?.confirmed && showAccelerationSummary">
<div class="status-panel d-flex flex-column h-100 w-100 justify-content-center align-items-center" [class.small-status]="tx && !tx.status?.confirmed && showAccelerationSummary">
@if (tx?.acceleration && !tx.status?.confirmed) {
<div class="progress-icon">
<fa-icon [icon]="['fas', 'wand-magic-sparkles']" [fixedWidth]="true"></fa-icon>
@@ -185,7 +185,11 @@
}
</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>
</div>
</div>

View File

@@ -49,7 +49,7 @@
position: relative;
background: var(--nav-bg);
box-shadow: 0 -5px 15px #000;
z-index: 100;
z-index: 99;
align-items: center;
justify-content: space-between;

View File

@@ -31,6 +31,8 @@ import { TrackerStage } from './tracker-bar.component';
import { MiningService, MiningStats } from '../../services/mining.service';
import { ETA, EtaService } from '../../services/eta.service';
import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
interface Pool {
id: number;
@@ -140,6 +142,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
private priceService: PriceService,
private enterpriseService: EnterpriseService,
private miningService: MiningService,
private router: Router,
private cd: ChangeDetectorRef,
private zone: NgZone,
@Inject(ZONE_SERVICE) private zoneService: any,
@@ -290,7 +293,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
})
).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;

View File

@@ -8,7 +8,7 @@
<div *ngIf="officialMempoolSpace">
<h2>Trademark Policy and Guidelines</h2>
<h5>The Mempool Open Source Project &reg;</h5>
<h6>Updated: July 3, 2024</h6>
<h6>Updated: August 19, 2024</h6>
<br>
<div class="text-left">
@@ -100,11 +100,26 @@
<p>The Mempool Accelerator Logo</p>
<br><br>
<img src="/resources/mempool-research.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool research Logo</p>
<br><br>
<app-svg-images name="goggles" height="96px"></app-svg-images>
<br><br>
<p>The Mempool Goggles Logo</p>
<br><br>
<img src="/resources/mempool-transaction.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool transaction Logo</p>
<br><br>
<img src="/resources/mempool-block-visualization.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool block visualization Logo</p>
<br><br>
<img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool Blocks Logo</p>

View File

@@ -62,6 +62,8 @@
</div>
}
<span id="accelerate"></span>
<ng-template [ngIf]="!isLoadingTx && !error">
<!-- CPFP Details -->
@@ -167,7 +169,7 @@
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
</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>
</ng-container>
@@ -550,19 +552,19 @@
<td>
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
@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>
@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>
} @else if (network === 'liquid' || network === 'liquidtestnet') {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
} @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>
@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 class="eta justify-content-end">
@@ -604,7 +606,12 @@
@if (!isLoadingTx) {
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></td>
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
}
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
</td>
</tr>
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
@@ -640,9 +647,9 @@
<td>
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) {
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
@if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) {

View File

@@ -297,7 +297,6 @@
}
.etaDeepMempool {
display: flex !important;
justify-content: flex-end;
flex-wrap: wrap;
align-content: center;
@@ -333,3 +332,7 @@
top: -1px;
margin-right: 2px;
}
.oobFees {
color: #905cf4;
}

View File

@@ -11,7 +11,9 @@ import {
tap,
map,
retry,
startWith
startWith,
repeat,
take
} from 'rxjs/operators';
import { Transaction } from '../../interfaces/electrs.interface';
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;
subscription: Subscription;
fetchCpfpSubscription: Subscription;
transactionTimesSubscription: Subscription;
fetchRbfSubscription: Subscription;
fetchCachedTxSubscription: Subscription;
fetchAccelerationSubscription: Subscription;
@@ -107,6 +110,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
showCpfpDetails = false;
miningStats: MiningStats;
fetchCpfp$ = new Subject<string>();
transactionTimes$ = new Subject<string>();
fetchRbfHistory$ = new Subject<string>();
fetchCachedTx$ = new Subject<string>();
fetchAcceleration$ = new Subject<number>();
@@ -226,6 +230,25 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
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$
.pipe(
switchMap((txId) =>
@@ -335,12 +358,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}),
).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;
this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
if (acceleration.txid === this.txId) {
if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') {
if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;
this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
} else {
this.tx.feeDelta = undefined;
}
}
this.waitingForAccelerationInfo = false;
this.setIsAccelerated();
}
@@ -388,10 +417,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
const isConflict = audit.fullrbfTxs.includes(txid);
const isExpected = audit.template.some(tx => tx.txid === txid);
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
const wasSeen = audit.version === 1 ? !audit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
return {
seen: isExpected || isPrioritized || isAccelerated,
seen: wasSeen,
expected: isExpected,
added: isAdded,
added: isAdded && (audit.version === 0 || !wasSeen),
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
@@ -438,9 +468,23 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (txPosition.position.acceleratedBy) {
txPosition.cpfp.acceleratedBy = txPosition.position.acceleratedBy;
}
if (txPosition.position.acceleratedAt) {
txPosition.cpfp.acceleratedAt = txPosition.position.acceleratedAt;
}
if (txPosition.position.feeDelta) {
txPosition.cpfp.feeDelta = txPosition.position.feeDelta;
}
this.setCpfpInfo(txPosition.cpfp);
} else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) {
this.tx.acceleratedBy = txPosition.position.acceleratedBy;
} else if ((this.tx?.acceleration)) {
if (txPosition.position.acceleratedBy) {
this.tx.acceleratedBy = txPosition.position.acceleratedBy;
}
if (txPosition.position.acceleratedAt) {
this.tx.acceleratedAt = txPosition.position.acceleratedAt;
}
if (txPosition.position.feeDelta) {
this.tx.feeDelta = txPosition.position.feeDelta;
}
}
if (this.stateService.network === '') {
@@ -495,6 +539,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(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
);
@@ -573,7 +625,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (tx.firstSeen) {
this.transactionTime = tx.firstSeen;
} else {
this.getTransactionTime();
this.transactionTimes$.next(tx.txid);
}
} else {
this.fetchAcceleration$.next(tx.status.block_height);
@@ -599,8 +651,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
});
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) >= 0.1));
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant || tx.descendants);
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && tx.effectiveFeePerVsize !== (this.tx.fee / (this.tx.weight / 4)) && tx.effectiveFeePerVsize !== (tx.fee / Math.ceil(tx.weight / 4)));
} else {
this.fetchCpfp$.next(this.tx.txid);
}
@@ -730,7 +782,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.accelerationPositions,
);
})
)
);
}
ngAfterViewInit(): void {
@@ -764,28 +816,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
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 {
if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null;
@@ -815,6 +845,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.acceleration = cpfpInfo.acceleration;
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
this.tx.feeDelta = cpfpInfo.feeDelta;
this.setIsAccelerated(firstCpfp);
}
@@ -829,8 +860,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.sigops = this.cpfpInfo.sigops;
this.adjustedVsize = this.cpfpInfo.adjustedVsize;
}
this.hasCpfp =!!(this.cpfpInfo && (this.cpfpInfo.bestDescendant || this.cpfpInfo.descendants?.length || this.cpfpInfo.ancestors?.length));
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
this.hasCpfp =!!(this.cpfpInfo && relatives.length);
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) {
@@ -842,7 +873,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
if (this.isAcceleration) {
// 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.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
});
@@ -1058,6 +1089,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnDestroy() {
this.subscription.unsubscribe();
this.fetchCpfpSubscription.unsubscribe();
this.transactionTimesSubscription.unsubscribe();
this.fetchRbfSubscription.unsubscribe();
this.fetchCachedTxSubscription.unsubscribe();
this.fetchAccelerationSubscription.unsubscribe();

View File

@@ -43,7 +43,7 @@
<span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
<span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span>
</ng-container>
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
<span *ngIf="line.type !== 'fee'"> #{{ line.index }}</span>
<ng-container [ngSwitch]="line.type">
<span *ngSwitchCase="'input'">
<ng-container *ngIf="line.status?.block_height">
@@ -73,7 +73,7 @@
<app-truncate [text]="line.txid"></app-truncate>
</p>
<ng-container [ngSwitch]="line.type">
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout + 1 }}
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout }}
<ng-container *ngIf="line.status?.block_height">
<ng-container *ngIf="line.blockHeight; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: line.blockHeight - line?.status?.block_height, connector: true}"></ng-container>
@@ -83,7 +83,7 @@
</ng-template>
</ng-container>
</p>
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin + 1 }}
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin }}
<ng-container *ngIf="line.blockHeight">
<ng-container *ngIf="line?.status?.block_height; else noBlockHeight">
<ng-container *ngTemplateOutlet="nBlocksLater; context:{n: line?.status?.block_height - line.blockHeight, connector: true}"></ng-container>

View File

@@ -17,11 +17,13 @@ export interface Transaction {
feePerVsize?: number;
effectiveFeePerVsize?: number;
ancestors?: Ancestor[];
descendants?: Ancestor[];
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
feeDelta?: number;
deleteAfter?: number;
_unblinded?: any;
_deduced?: boolean;

View File

@@ -31,6 +31,7 @@ export interface CpfpInfo {
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
feeDelta?: number;
}
export interface RbfInfo {
@@ -210,6 +211,8 @@ export interface BlockExtended extends Block {
}
export interface BlockAudit extends BlockExtended {
version: number,
unseenTxs?: string[],
missingTxs: string[],
addedTxs: string[],
prioritizedTxs: string[],
@@ -236,7 +239,7 @@ export interface TransactionStripped {
acc?: boolean;
flags?: number | null;
time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
}
@@ -250,6 +253,8 @@ export interface MempoolPosition {
vsize: number,
accelerated?: boolean,
acceleratedBy?: number[],
acceleratedAt?: number,
feeDelta?: number,
}
export interface AccelerationPosition extends MempoolPosition {
@@ -408,6 +413,7 @@ export interface Acceleration {
bidBoost?: number;
boostCost?: number;
boostRate?: number;
minedByPoolUniqueId?: number;
}
export interface AccelerationHistoryParams {

View File

@@ -7,7 +7,7 @@ export interface WebsocketResponse {
backend?: 'esplora' | 'electrum' | 'none';
block?: BlockExtended;
blocks?: BlockExtended[];
conversions?: any;
conversions?: Record<string, number>;
txConfirmed?: string;
historicalDate?: string;
mempoolInfo?: MempoolInfo;

View File

@@ -68,7 +68,7 @@ h3 {
.loading-spinner {
position: absolute;
top: 400px;
z-index: 100;
z-index: 99;
width: 100%;
left: 0;
@media (max-width: 767.98px) {

View File

@@ -16,7 +16,7 @@ import { StateService } from '../../services/state.service';
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
})

View File

@@ -19,7 +19,7 @@ import { StateService } from '../../services/state.service';
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
z-index: 99;
}
`],
})

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