Compare commits

...

579 Commits

Author SHA1 Message Date
wiz
0a116804e8 Merge pull request #5347 from mempool/junderw/fix-docker-aarch64
Bump Rust version to 1.79
2024-07-18 15:08:22 +09:00
junderw
68edf4306c Bump Rust version to 1.79
Maintaining an old MSRV is not a priority for this project.
If you would like to keep an old MSRV active, please maintain your own patch/fork.
2024-07-18 01:07:05 +09:00
softsimon
61bbb95819 fix default docker unix socket path variable 2024-07-17 12:34:28 +08:00
softsimon
3fa32edf25 Merge pull request #5345 from mempool/dependabot/npm_and_yarn/frontend/fortawesome/fontawesome-common-types-6.6.0
Bump @fortawesome/fontawesome-common-types from 6.5.1 to 6.6.0 in /frontend
2024-07-17 11:48:05 +09:00
dependabot[bot]
db220d9dfd Bump @fortawesome/fontawesome-common-types in /frontend
Bumps [@fortawesome/fontawesome-common-types](https://github.com/FortAwesome/Font-Awesome) from 6.5.1 to 6.6.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.5.1...6.6.0)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-05 02:38:18 +00:00
Mononaut
f77582250f Pizza tracker rbf tree traversal to find mined tx 2024-06-04 23:02:13 +00:00
Mononaut
976e505445 Pizza tracker handle RBF replacements 2024-06-04 22:45:43 +00:00
Mononaut
42c60fd991 Defer db access to fix failing tests 2024-06-04 20:57:40 +00:00
Mononaut
9a838c7269 Use estimated acceleration positions in frontend 2024-06-04 20:40:41 +00:00
Mononaut
f31b28251c Estimate accelerated positions in partner mempools 2024-06-04 20:40:40 +00:00
Mononaut
ced1595d70 research footer link 2024-06-04 15:50:13 +00:00
Mononaut
0b0109d821 Research unfurler preview image 2024-06-04 15:25:04 +00:00
Mononaut
992da1e5d2 Research logo 2024-06-04 15:09:45 +00:00
natsoni
25c0eb62b2 More robust price service 2024-06-04 10:58:04 +02:00
wiz
9b9aaed757 Merge pull request #5132 from mempool/mononaut/coldcard-nfc
Experimental auto-push URL support
2024-06-04 12:04:25 +09:00
Mononaut
b699063153 Experimental auto-push URL support 2024-06-03 21:45:36 +00:00
wiz
6947e19ca9 ops: Tweak nginx cache config 2024-06-03 18:21:14 +09:00
dependabot[bot]
9d4bbe9317 Bump node in /docker/frontend
Bumps node from 20.13.1-buster-slim to 20.14.0-buster-slim.

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 02:17:00 +00:00
softsimon
a0d3afb4d2 Merge pull request #5124 from mempool/natsoni/fix-lightning-search
Searchbar: wait for 3 characters before requesting lightning data
2024-06-01 14:22:19 +07:00
softsimon
67afda7dcf Merge branch 'master' into natsoni/fix-lightning-search 2024-06-01 14:20:00 +07:00
softsimon
a56af00500 Merge pull request #5123 from mempool/natsoni/search-results-ordering
Improve search results ordering
2024-06-01 14:19:48 +07:00
softsimon
e3971af207 Merge pull request #5122 from mempool/natsoni/fix-pool-ranking
Fix pool ranking table
2024-06-01 14:17:50 +07:00
Mononaut
37725bb341 Make address graph prefer "recent" mode by default 2024-05-31 17:20:07 +00:00
natsoni
f17635193a Fix pool ranking component update 2024-05-31 17:25:36 +02:00
softsimon
1c73dc59f9 Merge branch 'master' into natsoni/search-results-ordering 2024-05-31 22:18:43 +07:00
softsimon
3adbba2959 Merge branch 'master' into natsoni/fix-lightning-search 2024-05-31 21:20:31 +07:00
softsimon
ea1629fba8 Merge pull request #5121 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.10.0
Bump mysql2 from 3.9.7 to 3.10.0 in /backend
2024-05-31 21:20:02 +07:00
softsimon
87a4c087e5 Merge pull request #5118 from mempool/natsoni/fix-pool-page-update
Fix pool page update
2024-05-31 21:19:35 +07:00
softsimon
692edea1ce Merge branch 'master' into natsoni/fix-pool-page-update 2024-05-31 21:17:09 +07:00
softsimon
11cfb8a783 Merge pull request #5117 from mempool/natsoni/pools-search
Add mining pools to search results
2024-05-31 21:16:46 +07:00
natsoni
0b953f21b0 Only query lightning search if more than 3 characters 2024-05-31 15:40:27 +02:00
natsoni
d5508872dd Select lightning node by default in search results of public key 2024-05-31 15:08:58 +02:00
natsoni
321181d708 Update search results ordering 2024-05-31 13:52:37 +02:00
natsoni
f3bd50d4ab Revert "Update search results ordering"
This reverts commit 00838ea947.
2024-05-31 13:37:30 +02:00
dependabot[bot]
12a843c386 Bump mysql2 from 3.9.7 to 3.10.0 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.9.7 to 3.10.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.9.7...v3.10.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-05-31 02:34:09 +00:00
natsoni
21f91bcb6e Fix pool page update on slug change 2024-05-30 17:58:28 +02:00
natsoni
d57bd56743 Use includes() instead of startsWith() to search for pool names 2024-05-30 15:08:20 +02:00
natsoni
08969592ea Fix i18n for unknown pool search 2024-05-30 14:46:48 +02:00
wiz
f0437886ee Merge pull request #5116 from mempool/simon/fix-undefined-group-channels 2024-05-30 18:29:53 +09:00
softsimon
cfedb5fd24 Fix for undefined LN group channels 2024-05-30 16:15:51 +07:00
wiz
a9ad892495 Merge pull request #5112 from mempool/mononaut/polish-acc-pie
Polish acceleration pie chart section
2024-05-30 17:58:58 +09:00
natsoni
00838ea947 Update search results ordering 2024-05-30 10:34:40 +02:00
natsoni
7761ea53c6 Add mining pools to search bar 2024-05-30 09:31:44 +02:00
softsimon
aeeb4af9ba Merge pull request #5110 from mempool/natsoni/lift-up-blockchain-toggle
Slightly lift up blockchain toggle button
2024-05-29 16:21:45 +07:00
softsimon
9186f664da Merge pull request #5109 from mempool/natsoni/fix-mining-graphs
Fix widget mining graphs
2024-05-29 15:58:56 +07:00
softsimon
83db2a3b72 Add margin to graph on pool ranking page 2024-05-29 15:58:39 +07:00
natsoni
3cfd54b4c5 Update mining dashboard graph heights 2024-05-29 10:27:45 +02:00
Mononaut
c6db016c99 Show hashrate pie chart immediately on acceleration 2024-05-28 21:33:09 +00:00
Mononaut
6f6a9ea1a4 Brighter purple pie chart 2024-05-28 21:07:36 +00:00
Mononaut
83246be962 Responsive active acceleration details 2024-05-28 21:06:58 +00:00
natsoni
dcd94d868a Slightly lift up blockchain toggle button 2024-05-28 16:11:48 +02:00
natsoni
e9fc5c0433 Fix widget mining graphs 2024-05-28 16:11:06 +02:00
wiz
e281684ca4 Merge pull request #5107 from mempool/mononaut/acceleration-piechart-hotfix
Hotfix for acceleration pie chart section logic
2024-05-28 12:37:22 +09:00
Mononaut
6a915c0b88 Hotfix for acceleration pie chart section logic 2024-05-28 03:35:41 +00:00
wiz
078dc8d9a1 Merge pull request #5090 from mempool/mononaut/update-onbtc-preview-img
Update onbtc preview fallback image
2024-05-28 11:26:33 +09:00
wiz
232f81b906 Merge pull request #5017 from mempool/nymkappa/image-md5
[account] update profile image md5
2024-05-28 11:25:55 +09:00
wiz
8701119304 Merge pull request #5101 from mempool/natsoni/block-rewards-graph
Fees vs subsidy graph: add percentage mode
2024-05-28 11:23:57 +09:00
wiz
33c9f4a8dc Merge pull request #5103 from mempool/mononaut/multi-pool-acc
inline acceleration hashrate pie chart
2024-05-28 11:23:25 +09:00
natsoni
0654872627 Fix graph legend update while load bug and remove unnecessary query 2024-05-27 16:49:29 +02:00
natsoni
cca798eeaa Remove unnecessary filters in graph 2024-05-27 16:42:17 +02:00
Mononaut
1498db3b33 Backend support for multi-pool acceleration details 2024-05-26 20:47:36 +00:00
Mononaut
05b022dec8 multi-pool active accelerating details component 2024-05-26 20:39:35 +00:00
natsoni
6c6c18830c Fees vs subsidy graph: add percentage mode 2024-05-25 12:32:38 +02:00
Hans ❤️ Crypto
0e37e85af6 Create hans-crypto.txt 2024-05-21 12:35:20 +02:00
Hans ❤️ Crypto
4b3123b5ae Remove reference to bisq in unfurler
not needed anymore
2024-05-21 09:00:05 +02:00
Mononaut
69786d5b4b Update onbtc preview fallback image 2024-05-20 23:48:53 +00:00
nymkappa
0605e80d89 Merge branch 'master' into nymkappa/prepaid-update-price 2024-05-19 08:10:23 +02:00
Mononaut
568084e143 Configurable threshold for esplora tip check 2024-05-12 00:35:25 +00:00
nymkappa
8b1acbe13b [account] update profile image md5 2024-04-27 14:49:06 +02:00
wiz
6accf8420f Merge branch 'master' into nymkappa/prepaid-update-price 2024-04-25 02:24:42 +09:00
orangesurf
6e2c0bac43 Update accelerate-checkout.component.ts
Analysis suggests 1.5 would be a good starting point

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

View File

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

View File

@@ -181,7 +181,7 @@ Create a new wallet, if needed:
bitcoin-cli -regtest createwallet test
```
Load wallet (this command may take a while if you have lot of UTXOs):
Load wallet (this command may take a while if you have a lot of UTXOs):
```
bitcoin-cli -regtest loadwallet test
```
@@ -229,13 +229,13 @@ Generate block at regular interval (every 10 seconds in this example):
### Mining pools update
By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` is set to `false`).
By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` is set to `false`).
To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks.
You can enabled the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`.
You can enable the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` to `true` in your `mempool-config.json`.
When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionaly, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed.
When a `coinbase tag` or `coinbase address` change is detected, pool assignments for all relevant blocks (tagged to that pool or the `unknown` mining pool, starting from height 130635) are updated using the new criteria.
### Re-index tables

View File

@@ -24,7 +24,7 @@
"EXTERNAL_RETRY_INTERVAL": 0,
"USER_AGENT": "mempool",
"STDOUT_LOG_MIN_PRIORITY": "debug",
"AUTOMATIC_BLOCK_REINDEXING": false,
"AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"AUDIT": false,
@@ -59,7 +59,8 @@
"RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",

View File

@@ -18,12 +18,12 @@
"crypto-js": "~4.2.0",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.9.7",
"mysql2": "~3.10.0",
"redis": "^4.6.6",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.17.0"
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
@@ -6197,9 +6197,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz",
"integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -7690,9 +7690,9 @@
}
},
"node_modules/ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"engines": {
"node": ">=10.0.0"
},
@@ -12382,9 +12382,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz",
"integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==",
"requires": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -13424,9 +13424,9 @@
}
},
"ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"requires": {}
},
"y18n": {

View File

@@ -47,12 +47,12 @@
"crypto-js": "~4.2.0",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.9.7",
"mysql2": "~3.10.0",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.17.0"
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",

View File

@@ -10,7 +10,7 @@
"UNIX_SOCKET_PATH": "/mempool/socket/mempool-bitcoin-mainnet",
"SPAWN_CLUSTER_PROCS": 2,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"AUTOMATIC_BLOCK_REINDEXING": false,
"AUTOMATIC_POOLS_UPDATE": false,
"POLL_RATE_MS": 3,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CACHE_ENABLED": true,
@@ -60,7 +60,8 @@
"RETRY_UNIX_SOCKET_AFTER": 888,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ describe('Mempool Backend Config', () => {
UNIX_SOCKET_PATH: '',
SPAWN_CLUSTER_PROCS: 0,
API_URL_PREFIX: '/api/v1/',
AUTOMATIC_BLOCK_REINDEXING: false,
AUTOMATIC_POOLS_UPDATE: false,
POLL_RATE_MS: 2000,
CACHE_DIR: './cache',
CACHE_ENABLED: true,
@@ -63,6 +63,7 @@ describe('Mempool Backend Config', () => {
REQUEST_TIMEOUT: 10000,
FALLBACK_TIMEOUT: 5000,
FALLBACK: [],
MAX_BEHIND_TIP: 2,
});
expect(config.CORE_RPC).toStrictEqual({

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ export interface AbstractBitcoinApi {
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
startHealthChecks(): void;
getHealthStatus(): HealthCheckHost[];

View File

@@ -107,8 +107,14 @@ class BitcoinApi implements AbstractBitcoinApi {
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
}
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.');
async $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
const verboseBlock: IBitcoinApi.VerboseBlock = await this.bitcoindClient.getBlock(hash, 2);
const transactions: IEsploraApi.Transaction[] = [];
for (const tx of verboseBlock.tx) {
const converted = await this.$convertTransaction(tx, true);
transactions.push(converted);
}
return transactions;
}
$getRawBlock(hash: string): Promise<Buffer> {
@@ -159,13 +165,21 @@ class BitcoinApi implements AbstractBitcoinApi {
const mp = mempool.getMempool();
for (const tx in mp) {
for (const vout of mp[tx].vout) {
if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
if (vout.scriptpubkey_address?.indexOf(prefix) === 0) {
found[vout.scriptpubkey_address] = '';
if (Object.keys(found).length >= 10) {
return Object.keys(found);
}
}
}
for (const vin of mp[tx].vin) {
if (vin.prevout?.scriptpubkey_address?.indexOf(prefix) === 0) {
found[vin.prevout?.scriptpubkey_address] = '';
if (Object.keys(found).length >= 10) {
return Object.keys(found);
}
}
}
}
return Object.keys(found);
}
@@ -232,6 +246,11 @@ class BitcoinApi implements AbstractBitcoinApi {
return outspends;
}
async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> {
const txids = await this.$getTxIdsForBlock(blockhash);
return this.$getRawTransaction(txids[0]);
}
$getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);

View File

@@ -42,6 +42,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
@@ -160,7 +161,9 @@ class BitcoinRoutes {
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
sigops: tx.sigops,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined,
});
return;
}
@@ -359,6 +362,20 @@ class BitcoinRoutes {
}
}
private async $getBlockTxAuditSummary(req: Request, res: Response) {
try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
return res.status(404).send(`transaction audit not available`);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlocks(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin

View File

@@ -54,7 +54,7 @@ export namespace IEsploraApi {
scriptpubkey: string;
scriptpubkey_asm: string;
scriptpubkey_type: string;
scriptpubkey_address: string;
scriptpubkey_address?: string;
value: number;
// Elements
valuecommitment?: number;

View File

@@ -25,6 +25,7 @@ interface FailoverHost {
class FailoverRouter {
activeHost: FailoverHost;
fallbackHost: FailoverHost;
maxSlippage: number = config.ESPLORA.MAX_BEHIND_TIP ?? 2;
maxHeight: number = 0;
hosts: FailoverHost[];
multihost: boolean;
@@ -93,13 +94,13 @@ class FailoverRouter {
);
if (result) {
const height = result.data;
this.maxHeight = Math.max(height, this.maxHeight);
host.latestHeight = height;
this.maxHeight = Math.max(height || 0, ...this.hosts.map(h => (!(h.unreachable || h.timedOut || h.outOfSync) ? h.latestHeight || 0 : 0)));
const rtt = result.config['meta'].rtt;
host.rtts.unshift(rtt);
host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
host.latestHeight = height;
if (height == null || isNaN(height) || (this.maxHeight - height > 2)) {
if (height == null || isNaN(height) || (this.maxHeight - height > this.maxSlippage)) {
host.outOfSync = true;
} else {
host.outOfSync = false;
@@ -126,7 +127,6 @@ class FailoverRouter {
host.checked = true;
host.lastChecked = Date.now();
// switch if the current host is out of sync or significantly slower than the next best alternative
const rankOrder = this.sortHosts();
// switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== rankOrder[0] && rankOrder[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (rankOrder[0].rtt * 2) + 50)) {
@@ -184,7 +184,6 @@ class FailoverRouter {
// depose the active host and choose the next best replacement
private electHost(): void {
this.activeHost.outOfSync = true;
this.activeHost.failures = 0;
const rankOrder = this.sortHosts();
this.activeHost = rankOrder[0];
@@ -195,6 +194,7 @@ class FailoverRouter {
host.failures++;
if (host.failures > 5 && this.multihost) {
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
this.activeHost.unreachable = true;
this.electHost();
return this.activeHost;
} else {
@@ -352,6 +352,11 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json');
}
async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> {
const txid = await this.failoverRouter.$get<string>(`/block/${blockhash}/txid/0`);
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);
}
public startHealthChecks(): void {
this.failoverRouter.startHealthChecks();
}

View File

@@ -2,7 +2,7 @@ import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
@@ -295,10 +295,12 @@ class Blocks {
extras.virtualSize = block.weight / 4.0;
if (coinbaseTx?.vout.length > 0) {
extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null;
extras.coinbaseAddresses = [...new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[])];
extras.coinbaseSignature = coinbaseTx.vout[0].scriptpubkey_asm ?? null;
extras.coinbaseSignatureAscii = transactionUtils.hex2ascii(coinbaseTx.vin[0].scriptsig) ?? null;
} else {
extras.coinbaseAddress = null;
extras.coinbaseAddresses = null;
extras.coinbaseSignature = null;
extras.coinbaseSignatureAscii = null;
}
@@ -370,8 +372,7 @@ class Blocks {
}
}
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter((address) => address);
const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter(address => address) as string[];
let pools: PoolTag[] = [];
if (config.DATABASE.ENABLED === true) {
@@ -380,26 +381,9 @@ class Blocks {
pools = poolsParser.miningPools;
}
for (let i = 0; i < pools.length; ++i) {
if (addresses.length) {
const poolAddresses: string[] = typeof pools[i].addresses === 'string' ?
JSON.parse(pools[i].addresses) : pools[i].addresses;
for (let y = 0; y < poolAddresses.length; y++) {
if (addresses.indexOf(poolAddresses[y]) !== -1) {
return pools[i];
}
}
}
const regexes: string[] = typeof pools[i].regexes === 'string' ?
JSON.parse(pools[i].regexes) : pools[i].regexes;
for (let y = 0; y < regexes.length; ++y) {
const regex = new RegExp(regexes[y], 'i');
const match = asciiScriptSig.match(regex);
if (match !== null) {
return pools[i];
}
}
const pool = poolsParser.matchBlockMiner(txMinerInfo.vin[0].scriptsig, addresses || [], pools);
if (pool) {
return pool;
}
if (config.DATABASE.ENABLED === true) {
@@ -690,6 +674,52 @@ class Blocks {
this.classifyingBlocks = false;
}
/**
* [INDEXING] Index missing coinbase addresses for all blocks
*/
public async $indexCoinbaseAddresses(): Promise<void> {
try {
// Get all indexed block hash
const unindexedBlocks = await blocksRepository.$getBlocksWithoutCoinbaseAddresses();
if (!unindexedBlocks?.length) {
return;
}
logger.info(`Indexing missing coinbase addresses for ${unindexedBlocks.length} blocks`);
// Logging
let count = 0;
let countThisRun = 0;
let timer = Date.now() / 1000;
const startedAt = Date.now() / 1000;
for (const { height, hash } of unindexedBlocks) {
// Logging
const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5) {
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = countThisRun / elapsedSeconds;
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
logger.debug(`Indexing coinbase addresses for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`);
timer = Date.now() / 1000;
countThisRun = 0;
}
const coinbaseTx = await bitcoinApi.$getCoinbaseTx(hash);
const addresses = new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[]);
await blocksRepository.$saveCoinbaseAddresses(hash, [...addresses]);
// Logging
count++;
countThisRun++;
}
logger.notice(`coinbase addresses indexing completed: indexed ${count} blocks`);
} catch (e) {
logger.err(`coinbase addresses indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e;
}
}
/**
* [INDEXING] Index all blocks metadata for the mining dashboard
*/
@@ -1259,6 +1289,7 @@ class Blocks {
utxoset_size: block.extras.utxoSetSize ?? null,
coinbase_raw: block.extras.coinbaseRaw ?? null,
coinbase_address: block.extras.coinbaseAddress ?? null,
coinbase_addresses: block.extras.coinbaseAddresses ?? null,
coinbase_signature: block.extras.coinbaseSignature ?? null,
coinbase_signature_ascii: block.extras.coinbaseSignatureAscii ?? null,
pool_slug: block.extras.pool.slug ?? null,
@@ -1328,6 +1359,14 @@ class Blocks {
}
}
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
} else {
return null;
}
}
public getLastDifficultyAdjustmentTime(): number {
return this.lastDifficultyAdjustmentTime;
}

View File

@@ -258,9 +258,15 @@ export class Common {
let opreturnCount = 0;
for (const vout of tx.vout) {
// scriptpubkey
if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
// (non-standard output type)
return true;
} else if (vout.scriptpubkey_type === 'unknown') {
// undefined segwit version/length combinations are actually standard in outputs
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951
if (vout.scriptpubkey.startsWith('00') || !this.isWitnessProgram(vout.scriptpubkey)) {
return true;
}
} else if (vout.scriptpubkey_type === 'multisig') {
if (!DEFAULT_PERMIT_BAREMULTISIG) {
// bare-multisig
@@ -286,7 +292,7 @@ export class Common {
dustSize += getVarIntLength(dustSize);
// add value size
dustSize += 8;
if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) {
if (Common.isWitnessProgram(vout.scriptpubkey)) {
dustSize += 67;
} else {
dustSize += 148;
@@ -308,6 +314,27 @@ export class Common {
return false;
}
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
static isWitnessProgram(scriptpubkey: string): false | { version: number, program: string } {
if (scriptpubkey.length < 8 || scriptpubkey.length > 84) {
return false;
}
const version = parseInt(scriptpubkey.slice(0,2), 16);
if (version !== 0 && version < 0x51 || version > 0x60) {
return false;
}
const push = parseInt(scriptpubkey.slice(2,4), 16);
if (push + 2 === (scriptpubkey.length / 2)) {
return {
version: version ? version - 0x50 : 0,
program: scriptpubkey.slice(4),
};
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;
@@ -433,11 +460,10 @@ export class Common {
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
case 'v1_p2tr': {
if (!vin.witness?.length) {
throw new Error('Taproot input missing witness data');
}
flags |= TransactionFlags.p2tr;
flags = Common.isInscription(vin, flags);
if (vin.witness?.length) {
flags = Common.isInscription(vin, flags);
}
} break;
}
} else {
@@ -877,9 +903,10 @@ export class Common {
let medianFee = 0;
let medianWeight = 0;
// calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions
const leftBound = 1995000;
const rightBound = 2005000;
// calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of transactions
const halfWidth = config.MEMPOOL.BLOCK_WEIGHT_UNITS / 800;
const leftBound = Math.floor((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) - halfWidth);
const rightBound = Math.ceil((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) + halfWidth);
for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) {
const left = weightCount;
const right = weightCount + sortedTxs[i].weight;

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 79;
private static currentVersion = 80;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -686,6 +686,11 @@ class DatabaseMigration {
`);
await this.updateToSchemaVersion(79);
}
if (databaseSchemaVersion < 80) {
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
await this.updateToSchemaVersion(80);
}
}
/**

View File

@@ -1,11 +1,13 @@
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
import logger from '../logger';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates } from '../mempool.interfaces';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates, PoolTag } from '../mempool.interfaces';
import { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config';
import { Worker } from 'worker_threads';
import path from 'path';
import mempool from './mempool';
import { Acceleration } from './services/acceleration';
import PoolsRepository from '../repositories/PoolsRepository';
const MAX_UINT32 = Math.pow(2, 32) - 1;
@@ -14,12 +16,14 @@ class MempoolBlocks {
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
private txSelectionWorker: Worker | null = null;
private rustInitialized: boolean = false;
private rustGbtGenerator: GbtGenerator = new GbtGenerator();
private rustGbtGenerator: GbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
private nextUid: number = 1;
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
private txidMap: Map<string, number> = new Map(); // map full txids back to short numerical uids
private pools: { [id: number]: PoolTag } = {};
public getMempoolBlocks(): MempoolBlock[] {
return this.mempoolBlocks.map((block) => {
return {
@@ -41,6 +45,18 @@ class MempoolBlocks {
return this.mempoolBlockDeltas;
}
public async updatePools$(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
this.pools = {};
return;
}
const allPools = await PoolsRepository.$getPools();
this.pools = {};
for (const pool of allPools) {
this.pools[pool.uniqueId] = pool;
}
}
private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] {
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
@@ -214,7 +230,7 @@ class MempoolBlocks {
private resetRustGbt(): void {
this.rustInitialized = false;
this.rustGbtGenerator = new GbtGenerator();
this.rustGbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
}
public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
@@ -246,7 +262,7 @@ class MempoolBlocks {
});
// run the block construction algorithm in a separate thread, and wait for a result
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
try {
const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids(
await rustGbt.make(transactions as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
@@ -333,10 +349,13 @@ class MempoolBlocks {
}
}
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations: { [txid: string]: Acceleration }, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
for (const txid of Object.keys(candidates?.txs ?? mempool)) {
if (txid in mempool) {
mempool[txid].cpfpDirty = false;
mempool[txid].ancestors = [];
mempool[txid].descendants = [];
mempool[txid].bestDescendant = null;
}
}
for (const [txid, rate] of rates) {
@@ -396,7 +415,7 @@ class MempoolBlocks {
}
}
const isAccelerated : { [txid: string]: boolean } = {};
const isAcceleratedBy : { [txid: string]: number[] | false } = {};
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results
@@ -427,17 +446,21 @@ class MempoolBlocks {
};
const acceleration = accelerations[txid];
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
isAccelerated[ancestor.txid] = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
if (mempoolTx.acceleration) {
@@ -475,7 +498,7 @@ class MempoolBlocks {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
this.mempoolBlocks = mempoolBlocks;
this.mempoolBlockDeltas = deltas;
this.updateAccelerationPositions(mempool, accelerations, mempoolBlocks);
}
return mempoolBlocks;
@@ -622,6 +645,124 @@ class MempoolBlocks {
tx.acc ? 1 : 0,
];
}
// estimates and saves positions of accelerations in mining partner mempools
private updateAccelerationPositions(mempoolCache: { [txid: string]: MempoolTransactionExtended }, accelerations: { [txid: string]: Acceleration }, mempoolBlocks: MempoolBlockWithTransactions[]): void {
const accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
// keep track of simulated mempool blocks for each active pool
const pools: {
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
} = {};
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4);
}
return {
acceleration: acc,
rate: mempoolCache[acc.txid].effectiveFeePerVsize,
vsize
};
}).sort((a, b) => a.rate - b.rate);
// initialize the pool tracker
for (const { acceleration } of accQueue) {
accelerationPositions[acceleration.txid] = [];
for (const pool of acceleration.pools) {
if (!pools[pool]) {
pools[pool] = {
name: this.pools[pool]?.name || 'unknown',
block: 0,
vsize: 0,
accelerations: [],
complete: false,
};
}
pools[pool].accelerations.push(acceleration.txid);
}
for (const ancestor of mempoolCache[acceleration.txid].ancestors || []) {
accelerationPositions[ancestor.txid] = [];
}
}
for (const pool of Object.keys(pools)) {
// if any pools accepted *every* acceleration, we can just use the GBT result positions directly
if (pools[pool].accelerations.length === Object.keys(accelerations).length) {
pools[pool].complete = true;
}
}
let block = 0;
let index = 0;
let next = accQueue.pop();
// build simulated blocks for each pool by taking the best option from
// either the mempool or the list of accelerations.
while (next && block < mempoolBlocks.length) {
while (next && index < mempoolBlocks[block].transactions.length) {
const nextTx = mempoolBlocks[block].transactions[index];
if (next.rate >= (nextTx.rate || (nextTx.fee / nextTx.vsize))) {
for (const pool of next.acceleration.pools) {
if (pools[pool].vsize + next.vsize <= 999_000) {
pools[pool].vsize += next.vsize;
} else {
pools[pool].block++;
pools[pool].vsize = next.vsize;
}
// insert the acceleration into matching pool's blocks
if (pools[pool].complete && mempoolCache[next.acceleration.txid]?.position !== undefined) {
accelerationPositions[next.acceleration.txid].push({
...mempoolCache[next.acceleration.txid].position as { block: number, vsize: number },
poolId: pool,
pool: pools[pool].name
});
} else {
accelerationPositions[next.acceleration.txid].push({
poolId: pool,
pool: pools[pool].name,
block: pools[pool].block,
vsize: pools[pool].vsize - (next.vsize / 2),
});
}
// and any accelerated ancestors
for (const ancestor of mempoolCache[next.acceleration.txid].ancestors || []) {
if (pools[pool].complete && mempoolCache[ancestor.txid]?.position !== undefined) {
accelerationPositions[ancestor.txid].push({
...mempoolCache[ancestor.txid].position as { block: number, vsize: number },
poolId: pool,
pool: pools[pool].name,
});
} else {
accelerationPositions[ancestor.txid].push({
poolId: pool,
pool: pools[pool].name,
block: pools[pool].block,
vsize: pools[pool].vsize - (next.vsize / 2),
});
}
}
}
next = accQueue.pop();
} else {
// skip accelerated transactions and their CPFP ancestors
if (accelerationPositions[nextTx.txid] == null) {
// insert into all pools' blocks
for (const pool of Object.keys(pools)) {
if (pools[pool].vsize + nextTx.vsize <= 999_000) {
pools[pool].vsize += nextTx.vsize;
} else {
pools[pool].block++;
pools[pool].vsize = nextTx.vsize;
}
}
}
index++;
}
}
block++;
index = 0;
}
mempool.setAccelerationPositions(accelerationPositions);
}
}
export default new MempoolBlocks();

View File

@@ -27,6 +27,7 @@ class Mempool {
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
private txPerSecondArray: number[] = [];
private txPerSecond: number = 0;
@@ -514,6 +515,14 @@ class Mempool {
}
}
setAccelerationPositions(positions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] }): void {
this.accelerationPositions = positions;
}
getAccelerationPositions(txid: string): { [pool: number]: { poolId: number, pool: string, block: number, vsize: number } } | undefined {
return this.accelerationPositions[txid];
}
private startTimer() {
const state: any = {
start: Date.now(),

View File

@@ -227,10 +227,8 @@ class MiningRoutes {
throw new Error('from must be less than to');
}
const blockFees = await mining.$getBlockFeesTimespan(parseInt(req.query.from as string, 10), parseInt(req.query.to as string, 10));
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {

View File

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

View File

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

View File

@@ -10,6 +10,12 @@ export interface Acceleration {
effectiveFee: number,
feeDelta: number,
pools: number[],
positions?: {
[pool: number]: {
block: number,
vbytes: number,
},
},
};
export interface AccelerationHistory {
@@ -25,10 +31,7 @@ export interface AccelerationHistory {
feeDelta: number,
blockHash: string,
blockHeight: number,
pools: {
pool_unique_id: number,
username: string,
}[],
pools: number[];
};
class AccelerationApi {

View File

@@ -206,7 +206,8 @@ class WebsocketHandler {
}
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position
position,
accelerationPositions: memPool.getAccelerationPositions(tx.txid),
});
}
} else {
@@ -820,7 +821,10 @@ class WebsocketHandler {
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
};
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
calculateCpfp(mempoolTx, newMempool);
@@ -833,7 +837,7 @@ class WebsocketHandler {
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
sigops: mempoolTx.sigops,
adjustedVsize: mempoolTx.adjustedVsize,
acceleration: mempoolTx.acceleration
acceleration: mempoolTx.acceleration,
};
}
response['txPosition'] = JSON.stringify(positionData);
@@ -858,6 +862,8 @@ class WebsocketHandler {
txInfo.position = {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
};
if (!mempoolTx.cpfpChecked) {
calculateCpfp(mempoolTx, newMempool);
@@ -1134,7 +1140,10 @@ class WebsocketHandler {
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
});
}
}
@@ -1153,6 +1162,8 @@ class WebsocketHandler {
...mempoolTx.position,
},
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
};
}
}
@@ -1293,7 +1304,7 @@ class WebsocketHandler {
// and zips it together into a valid JSON object
private serializeResponse(response): string {
return '{'
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
+ Object.keys(response).filter(key => response[key] != null).map(key => `"${key}": ${response[key]}`).join(', ')
+ '}';
}

View File

@@ -29,7 +29,7 @@ interface IConfig {
EXTERNAL_RETRY_INTERVAL: number;
USER_AGENT: string;
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
AUTOMATIC_BLOCK_REINDEXING: boolean;
AUTOMATIC_POOLS_UPDATE: boolean;
POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string,
AUDIT: boolean;
@@ -51,6 +51,7 @@ interface IConfig {
REQUEST_TIMEOUT: number;
FALLBACK_TIMEOUT: number;
FALLBACK: string[];
MAX_BEHIND_TIP: number;
};
LIGHTNING: {
ENABLED: boolean;
@@ -188,7 +189,7 @@ const defaults: IConfig = {
'EXTERNAL_RETRY_INTERVAL': 0,
'USER_AGENT': 'mempool',
'STDOUT_LOG_MIN_PRIORITY': 'debug',
'AUTOMATIC_BLOCK_REINDEXING': false,
'AUTOMATIC_POOLS_UPDATE': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
'AUDIT': false,
@@ -210,6 +211,7 @@ const defaults: IConfig = {
'REQUEST_TIMEOUT': 10000,
'FALLBACK_TIMEOUT': 5000,
'FALLBACK': [],
'MAX_BEHIND_TIP': 2,
},
'ELECTRUM': {
'HOST': '127.0.0.1',

View File

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

View File

@@ -45,6 +45,7 @@ import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks';
class Server {
private wss: WebSocket.Server | undefined;
@@ -149,6 +150,7 @@ class Server {
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$();
await mempoolBlocks.updatePools$();
if (config.MEMPOOL.ENABLED) {
if (config.MEMPOOL.CACHE_ENABLED) {
await diskCache.$loadMempoolCache();
@@ -331,7 +333,9 @@ class Server {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app);
}
aboutRoutes.initRoutes(this.app);
if (!config.MEMPOOL.OFFICIAL) {
aboutRoutes.initRoutes(this.app);
}
}
healthCheck(): void {

View File

@@ -182,6 +182,7 @@ class Indexer {
}
this.runSingleTask('blocksPrices');
await blocks.$indexCoinbaseAddresses();
await mining.$indexDifficultyAdjustments();
await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory();

View File

@@ -42,6 +42,19 @@ export interface BlockAudit {
matchRate: number,
expectedFees?: number,
expectedWeight?: number,
template?: any[];
}
export interface TransactionAudit {
seen?: boolean;
expected?: boolean;
added?: boolean;
prioritized?: boolean;
delayed?: number;
accelerated?: boolean;
conflict?: boolean;
coinbase?: boolean;
firstSeen?: number;
}
export interface AuditScore {
@@ -111,6 +124,8 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
vsize: number,
};
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
replacement?: boolean;
uid?: number;
flags?: number;
@@ -286,6 +301,7 @@ export interface BlockExtension {
coinbaseRaw: string;
orphans: OrphanedBlock[] | null;
coinbaseAddress: string | null;
coinbaseAddresses: string[] | null;
coinbaseSignature: string | null;
coinbaseSignatureAscii: string | null;
virtualSize: number;
@@ -432,7 +448,7 @@ export interface OptimizedStatistic {
export interface TxTrackingInfo {
replacedBy?: string,
position?: { block: number, vsize: number, accelerated?: boolean },
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number },
cpfp?: {
ancestors?: Ancestor[],
bestDescendant?: Ancestor | null,
@@ -443,6 +459,8 @@ export interface TxTrackingInfo {
},
utxoSpent?: { [vout: number]: { vin: number, txid: string } },
accelerated?: boolean,
acceleratedBy?: number[],
acceleratedAt?: number,
confirmed?: boolean
}

View File

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

View File

@@ -308,10 +308,10 @@ class AccelerationRepository {
}
const accelerationSummaries = accelerations.map(acc => ({
...acc,
pools: acc.pools.map(pool => pool.pool_unique_id),
pools: acc.pools,
}))
for (const acc of accelerations) {
if (blockTxs[acc.txid] && acc.pools.some(pool => pool.pool_unique_id === block.extras.pool.id)) {
if (blockTxs[acc.txid] && acc.pools.includes(block.extras.pool.id)) {
const tx = blockTxs[acc.txid];
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));

View File

@@ -1,7 +1,7 @@
import blocks from '../api/blocks';
import DB from '../database';
import logger from '../logger';
import { BlockAudit, AuditScore } from '../mempool.interfaces';
import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> {
@@ -98,6 +98,41 @@ class BlocksAuditRepositories {
}
}
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
try {
const blockAudit = await this.$getBlockAudit(hash);
if (blockAudit) {
const isAdded = blockAudit.addedTxs.includes(txid);
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
const isAccelerated = blockAudit.acceleratedTxs.includes(txid);
const isConflict = blockAudit.fullrbfTxs.includes(txid);
let isExpected = false;
let firstSeen = undefined;
blockAudit.template?.forEach(tx => {
if (tx.txid === txid) {
isExpected = true;
firstSeen = tx.time;
}
});
return {
seen: isExpected || isPrioritized || isAccelerated,
expected: isExpected,
added: isAdded,
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
firstSeen,
}
}
return null;
} catch (e: any) {
logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
try {
const [rows]: any[] = await DB.query(

View File

@@ -5,7 +5,7 @@ import logger from '../logger';
import { Common } from '../api/common';
import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository';
import { RowDataPacket, escape } from 'mysql2';
import { RowDataPacket } from 'mysql2';
import BlocksSummariesRepository from './BlocksSummariesRepository';
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
import bitcoinClient from '../api/bitcoin/bitcoin-client';
@@ -40,6 +40,7 @@ interface DatabaseBlock {
avgFeeRate: number;
coinbaseRaw: string;
coinbaseAddress: string;
coinbaseAddresses: string;
coinbaseSignature: string;
coinbaseSignatureAscii: string;
avgTxSize: number;
@@ -82,6 +83,7 @@ const BLOCK_DB_FIELDS = `
blocks.avg_fee_rate AS avgFeeRate,
blocks.coinbase_raw AS coinbaseRaw,
blocks.coinbase_address AS coinbaseAddress,
blocks.coinbase_addresses AS coinbaseAddresses,
blocks.coinbase_signature AS coinbaseSignature,
blocks.coinbase_signature_ascii AS coinbaseSignatureAscii,
blocks.avg_tx_size AS avgTxSize,
@@ -114,7 +116,7 @@ class BlocksRepository {
pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address,
median_timestamp, header, coinbase_address, coinbase_addresses,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
@@ -125,7 +127,7 @@ class BlocksRepository {
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
FROM_UNIXTIME(?), ?, ?,
FROM_UNIXTIME(?), ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
@@ -161,6 +163,7 @@ class BlocksRepository {
block.mediantime,
block.extras.header,
block.extras.coinbaseAddress,
block.extras.coinbaseAddresses ? JSON.stringify(block.extras.coinbaseAddresses) : null,
truncatedCoinbaseSignature,
block.extras.utxoSetSize,
block.extras.utxoSetChange,
@@ -529,7 +532,7 @@ class BlocksRepository {
return null;
}
return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);
return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);
} catch (e) {
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
@@ -922,6 +925,25 @@ class BlocksRepository {
}
}
/**
* Get all indexed blocks with missing coinbase addresses
*/
public async $getBlocksWithoutCoinbaseAddresses(): Promise<any> {
try {
const [blocks] = await DB.query(`
SELECT height, hash, coinbase_addresses
FROM blocks
WHERE coinbase_addresses IS NULL AND
coinbase_address IS NOT NULL
ORDER BY height DESC
`);
return blocks;
} catch (e) {
logger.err(`Cannot get blocks with missing coinbase addresses. Reason: ` + (e instanceof Error ? e.message : e));
return [];
}
}
/**
* Save indexed median fee to avoid recomputing it later
*
@@ -960,6 +982,44 @@ class BlocksRepository {
}
}
/**
* Save coinbase addresses
*
* @param id
* @param addresses
*/
public async $saveCoinbaseAddresses(id: string, addresses: string[]): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET coinbase_addresses = ?
WHERE hash = ?`,
[JSON.stringify(addresses), id]
);
} catch (e) {
logger.err(`Cannot update block coinbase addresses. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Save pool
*
* @param id
* @param poolId
*/
public async $savePool(id: string, poolId: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET pool_id = ?
WHERE hash = ?`,
[poolId, id]
);
} catch (e) {
logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Convert a mysql row block into a BlockExtended. Note that you
* must provide the correct field into dbBlk object param
@@ -999,6 +1059,7 @@ class BlocksRepository {
extras.avgFeeRate = dbBlk.avgFeeRate;
extras.coinbaseRaw = dbBlk.coinbaseRaw;
extras.coinbaseAddress = dbBlk.coinbaseAddress;
extras.coinbaseAddresses = dbBlk.coinbaseAddresses ? JSON.parse(dbBlk.coinbaseAddresses) : [];
extras.coinbaseSignature = dbBlk.coinbaseSignature;
extras.coinbaseSignatureAscii = dbBlk.coinbaseSignatureAscii;
extras.avgTxSize = dbBlk.avgTxSize;

View File

@@ -50,10 +50,10 @@ class PoolsUpdater {
// See backend README for more details about the mining pools update process
if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool
) {
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_BLOCK_REINDEXING is disabled`);
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`);
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`);
return;
}

View File

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

View File

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

View File

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

View File

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

3
contributors/jlopp.txt Normal file
View File

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

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 18th, 2024.
Signed: mackalex

3
contributors/svrgnty.txt Normal file
View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
"GOGGLES_INDEXING": __MEMPOOL_GOGGLES_INDEXING__,
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__,
"AUTOMATIC_POOLS_UPDATE": __MEMPOOL_AUTOMATIC_POOLS_UPDATE__,
"AUDIT": __MEMPOOL_AUDIT__,
"RUST_GBT": __MEMPOOL_RUST_GBT__,
"LIMIT_GBT": __MEMPOOL_LIMIT_GBT__,
@@ -60,7 +60,8 @@
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
"FALLBACK": __ESPLORA_FALLBACK__
"FALLBACK": __ESPLORA_FALLBACK__,
"MAX_BEHIND_TIP": __ESPLORA_MAX_BEHIND_TIP__
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",

View File

@@ -26,7 +26,7 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
@@ -62,6 +62,7 @@ __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
__ESPLORA_MAX_BEHIND_TIP__=${ESPLORA_MAX_BEHIND_TIP:=2}
# SECOND_CORE_RPC
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
@@ -148,7 +149,7 @@ __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
# FIAT_PRICE
@@ -183,7 +184,7 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__!${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
@@ -216,6 +217,7 @@ sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTE
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
sed -i "s!__ESPLORA_MAX_BEHIND_TIP__!${__ESPLORA_MAX_BEHIND_TIP__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json

View File

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

View File

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

View File

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

View File

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

View File

@@ -144,13 +144,13 @@ describe('Mainnet', () => {
});
});
['BC1PQYQSZQ', 'bc1PqYqSzQ'].forEach((searchTerm) => {
['BC1PQYQS', 'bc1PqYqS'].forEach((searchTerm) => {
it(`allows searching for partial case insensitive bech32m addresses: ${searchTerm}`, () => {
cy.visit('/');
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
cy.get('app-search-results button.dropdown-item').should('have.length', 1);
cy.get('app-search-results button.dropdown-item').should('have.length', 10);
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
cy.url().should('include', '/address/bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e');
cy.url().should('include', '/address/bc1pqyqs26fs4gnyw4aqttyjqa5ta7075zzfjftyz98qa8vdr49dh7fqm2zkv3');
cy.waitForSkeletonGone();
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
});
@@ -158,13 +158,13 @@ describe('Mainnet', () => {
});
});
['BC1Q000375VXCU', 'bC1q000375vXcU'].forEach((searchTerm) => {
['BC1Q0003', 'bC1q0003'].forEach((searchTerm) => {
it(`allows searching for partial case insensitive bech32 addresses: ${searchTerm}`, () => {
cy.visit('/');
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
cy.get('app-search-results button.dropdown-item').should('have.length', 1);
cy.get('app-search-results button.dropdown-item').should('have.length', 10);
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
cy.url().should('include', '/address/bc1q000375vxcuf5v04lmwy22vy2thvhqkxghgq7dy');
cy.url().should('include', '/address/bc1q000303cgr9zazthut63kdktwtatfe206um8nyh');
cy.waitForSkeletonGone();
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
});

View File

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

View File

@@ -23,9 +23,9 @@
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.5.1",
"@fortawesome/fontawesome-svg-core": "~6.5.1",
"@fortawesome/free-solid-svg-icons": "~6.5.1",
"@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0",
@@ -34,7 +34,7 @@
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.5.0",
"esbuild": "^0.21.1",
"esbuild": "^0.23.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
@@ -62,7 +62,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.10.0",
"cypress": "^13.13.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
@@ -3196,9 +3196,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
"integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz",
"integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==",
"cpu": [
"ppc64"
],
@@ -3207,13 +3207,13 @@
"aix"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz",
"integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz",
"integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==",
"cpu": [
"arm"
],
@@ -3222,13 +3222,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
"integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz",
"integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==",
"cpu": [
"arm64"
],
@@ -3237,13 +3237,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
"integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz",
"integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==",
"cpu": [
"x64"
],
@@ -3252,13 +3252,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
"integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz",
"integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==",
"cpu": [
"arm64"
],
@@ -3267,13 +3267,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz",
"integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz",
"integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==",
"cpu": [
"x64"
],
@@ -3282,13 +3282,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
"integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz",
"integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==",
"cpu": [
"arm64"
],
@@ -3297,13 +3297,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
"integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz",
"integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==",
"cpu": [
"x64"
],
@@ -3312,13 +3312,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
"integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz",
"integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==",
"cpu": [
"arm"
],
@@ -3327,13 +3327,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
"integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz",
"integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==",
"cpu": [
"arm64"
],
@@ -3342,13 +3342,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz",
"integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz",
"integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==",
"cpu": [
"ia32"
],
@@ -3357,13 +3357,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
"integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz",
"integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==",
"cpu": [
"loong64"
],
@@ -3372,13 +3372,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
"integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz",
"integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==",
"cpu": [
"mips64el"
],
@@ -3387,13 +3387,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
"integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz",
"integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==",
"cpu": [
"ppc64"
],
@@ -3402,13 +3402,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
"integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz",
"integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==",
"cpu": [
"riscv64"
],
@@ -3417,13 +3417,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
"integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz",
"integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==",
"cpu": [
"s390x"
],
@@ -3432,13 +3432,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
"integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz",
"integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==",
"cpu": [
"x64"
],
@@ -3447,13 +3447,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
"integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz",
"integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==",
"cpu": [
"x64"
],
@@ -3462,13 +3462,28 @@
"netbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz",
"integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz",
"integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz",
"integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==",
"cpu": [
"x64"
],
@@ -3477,13 +3492,13 @@
"openbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
"integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz",
"integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==",
"cpu": [
"x64"
],
@@ -3492,13 +3507,13 @@
"sunos"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz",
"integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz",
"integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==",
"cpu": [
"arm64"
],
@@ -3507,13 +3522,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
"integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz",
"integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==",
"cpu": [
"ia32"
],
@@ -3522,13 +3537,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
"integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz",
"integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==",
"cpu": [
"x64"
],
@@ -3537,7 +3552,7 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -3654,33 +3669,30 @@
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
"integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
"hasInstallScript": true,
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
"integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
"hasInstallScript": true,
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.1"
"@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
"integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
"hasInstallScript": true,
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.1"
"@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
@@ -6105,11 +6117,11 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -8028,9 +8040,9 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.10.0.tgz",
"integrity": "sha512-tOhwRlurVOQbMduX+KonoMeQILs2cwR3yHGGENoFvvSoLUBHmJ8b9/n21gFSDqjlOJ+SRVcwuh+fG/JDsHsT6Q==",
"version": "13.13.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@@ -8073,7 +8085,7 @@
"request-progress": "^3.0.0",
"semver": "^7.5.3",
"supports-color": "^8.1.1",
"tmp": "~0.2.1",
"tmp": "~0.2.3",
"untildify": "^4.0.0",
"yauzl": "^2.10.0"
},
@@ -8250,15 +8262,12 @@
}
},
"node_modules/cypress/node_modules/tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
"optional": true,
"dependencies": {
"rimraf": "^3.0.0"
},
"engines": {
"node": ">=8.17.0"
"node": ">=14.14"
}
},
"node_modules/d": {
@@ -9196,40 +9205,41 @@
}
},
"node_modules/esbuild": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
"integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz",
"integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.1",
"@esbuild/android-arm": "0.21.1",
"@esbuild/android-arm64": "0.21.1",
"@esbuild/android-x64": "0.21.1",
"@esbuild/darwin-arm64": "0.21.1",
"@esbuild/darwin-x64": "0.21.1",
"@esbuild/freebsd-arm64": "0.21.1",
"@esbuild/freebsd-x64": "0.21.1",
"@esbuild/linux-arm": "0.21.1",
"@esbuild/linux-arm64": "0.21.1",
"@esbuild/linux-ia32": "0.21.1",
"@esbuild/linux-loong64": "0.21.1",
"@esbuild/linux-mips64el": "0.21.1",
"@esbuild/linux-ppc64": "0.21.1",
"@esbuild/linux-riscv64": "0.21.1",
"@esbuild/linux-s390x": "0.21.1",
"@esbuild/linux-x64": "0.21.1",
"@esbuild/netbsd-x64": "0.21.1",
"@esbuild/openbsd-x64": "0.21.1",
"@esbuild/sunos-x64": "0.21.1",
"@esbuild/win32-arm64": "0.21.1",
"@esbuild/win32-ia32": "0.21.1",
"@esbuild/win32-x64": "0.21.1"
"@esbuild/aix-ppc64": "0.23.0",
"@esbuild/android-arm": "0.23.0",
"@esbuild/android-arm64": "0.23.0",
"@esbuild/android-x64": "0.23.0",
"@esbuild/darwin-arm64": "0.23.0",
"@esbuild/darwin-x64": "0.23.0",
"@esbuild/freebsd-arm64": "0.23.0",
"@esbuild/freebsd-x64": "0.23.0",
"@esbuild/linux-arm": "0.23.0",
"@esbuild/linux-arm64": "0.23.0",
"@esbuild/linux-ia32": "0.23.0",
"@esbuild/linux-loong64": "0.23.0",
"@esbuild/linux-mips64el": "0.23.0",
"@esbuild/linux-ppc64": "0.23.0",
"@esbuild/linux-riscv64": "0.23.0",
"@esbuild/linux-s390x": "0.23.0",
"@esbuild/linux-x64": "0.23.0",
"@esbuild/netbsd-x64": "0.23.0",
"@esbuild/openbsd-arm64": "0.23.0",
"@esbuild/openbsd-x64": "0.23.0",
"@esbuild/sunos-x64": "0.23.0",
"@esbuild/win32-arm64": "0.23.0",
"@esbuild/win32-ia32": "0.23.0",
"@esbuild/win32-x64": "0.23.0"
}
},
"node_modules/esbuild-wasm": {
@@ -10151,9 +10161,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -20562,141 +20572,147 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="
},
"@esbuild/aix-ppc64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
"integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz",
"integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==",
"optional": true
},
"@esbuild/android-arm": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.1.tgz",
"integrity": "sha512-hh3jKWikdnTtHCglDAeVO3Oyh8MaH8xZUaWMiCCvJ9/c3NtPqZq+CACOlGTxhddypXhl+8B45SeceYBfB/e8Ow==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz",
"integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==",
"optional": true
},
"@esbuild/android-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
"integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz",
"integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==",
"optional": true
},
"@esbuild/android-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
"integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz",
"integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==",
"optional": true
},
"@esbuild/darwin-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
"integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz",
"integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==",
"optional": true
},
"@esbuild/darwin-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.1.tgz",
"integrity": "sha512-D3h3wBQmeS/vp93O4B+SWsXB8HvRDwMyhTNhBd8yMbh5wN/2pPWRW5o/hM3EKgk9bdKd9594lMGoTCTiglQGRQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz",
"integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==",
"optional": true
},
"@esbuild/freebsd-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
"integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz",
"integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==",
"optional": true
},
"@esbuild/freebsd-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
"integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz",
"integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==",
"optional": true
},
"@esbuild/linux-arm": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
"integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz",
"integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==",
"optional": true
},
"@esbuild/linux-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
"integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz",
"integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==",
"optional": true
},
"@esbuild/linux-ia32": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.1.tgz",
"integrity": "sha512-tt/54LqNNAqCz++QhxoqB9+XqdsaZOtFD/srEhHYwBd3ZUOepmR1Eeot8bS+Q7BiEvy9vvKbtpHf+r6q8hF5UA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz",
"integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==",
"optional": true
},
"@esbuild/linux-loong64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
"integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz",
"integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==",
"optional": true
},
"@esbuild/linux-mips64el": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
"integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz",
"integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==",
"optional": true
},
"@esbuild/linux-ppc64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
"integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz",
"integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==",
"optional": true
},
"@esbuild/linux-riscv64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
"integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz",
"integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==",
"optional": true
},
"@esbuild/linux-s390x": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
"integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz",
"integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==",
"optional": true
},
"@esbuild/linux-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
"integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz",
"integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==",
"optional": true
},
"@esbuild/netbsd-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
"integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz",
"integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==",
"optional": true
},
"@esbuild/openbsd-arm64": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz",
"integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==",
"optional": true
},
"@esbuild/openbsd-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.1.tgz",
"integrity": "sha512-xxhTm5QtzNLc24R0hEkcH+zCx/o49AsdFZ0Cy5zSd/5tOj4X2g3/2AJB625NoadUuc4A8B3TenLJoYdWYOYCew==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz",
"integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==",
"optional": true
},
"@esbuild/sunos-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
"integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz",
"integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==",
"optional": true
},
"@esbuild/win32-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.1.tgz",
"integrity": "sha512-jb5B4k+xkytGbGUS4T+Z89cQJ9DJ4lozGRSV+hhfmCPpfJ3880O31Q1srPCimm+V6UCbnigqD10EgDNgjvjerQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz",
"integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==",
"optional": true
},
"@esbuild/win32-ia32": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
"integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz",
"integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==",
"optional": true
},
"@esbuild/win32-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
"integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz",
"integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==",
"optional": true
},
"@eslint-community/eslint-utils": {
@@ -20778,24 +20794,24 @@
}
},
"@fortawesome/fontawesome-common-types": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
"integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A=="
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw=="
},
"@fortawesome/fontawesome-svg-core": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
"integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
"requires": {
"@fortawesome/fontawesome-common-types": "6.5.1"
"@fortawesome/fontawesome-common-types": "6.6.0"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
"integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
"requires": {
"@fortawesome/fontawesome-common-types": "6.5.1"
"@fortawesome/fontawesome-common-types": "6.6.0"
}
},
"@goto-bus-stop/common-shake": {
@@ -22635,11 +22651,11 @@
}
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"requires": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
}
},
"brorand": {
@@ -24111,9 +24127,9 @@
"peer": true
},
"cypress": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.10.0.tgz",
"integrity": "sha512-tOhwRlurVOQbMduX+KonoMeQILs2cwR3yHGGENoFvvSoLUBHmJ8b9/n21gFSDqjlOJ+SRVcwuh+fG/JDsHsT6Q==",
"version": "13.13.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.0",
@@ -24155,7 +24171,7 @@
"request-progress": "^3.0.0",
"semver": "^7.5.3",
"supports-color": "^8.1.1",
"tmp": "~0.2.1",
"tmp": "~0.2.3",
"untildify": "^4.0.0",
"yauzl": "^2.10.0"
},
@@ -24265,13 +24281,10 @@
}
},
"tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"optional": true,
"requires": {
"rimraf": "^3.0.0"
}
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
"optional": true
}
}
},
@@ -25031,33 +25044,34 @@
}
},
"esbuild": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
"integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz",
"integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==",
"requires": {
"@esbuild/aix-ppc64": "0.21.1",
"@esbuild/android-arm": "0.21.1",
"@esbuild/android-arm64": "0.21.1",
"@esbuild/android-x64": "0.21.1",
"@esbuild/darwin-arm64": "0.21.1",
"@esbuild/darwin-x64": "0.21.1",
"@esbuild/freebsd-arm64": "0.21.1",
"@esbuild/freebsd-x64": "0.21.1",
"@esbuild/linux-arm": "0.21.1",
"@esbuild/linux-arm64": "0.21.1",
"@esbuild/linux-ia32": "0.21.1",
"@esbuild/linux-loong64": "0.21.1",
"@esbuild/linux-mips64el": "0.21.1",
"@esbuild/linux-ppc64": "0.21.1",
"@esbuild/linux-riscv64": "0.21.1",
"@esbuild/linux-s390x": "0.21.1",
"@esbuild/linux-x64": "0.21.1",
"@esbuild/netbsd-x64": "0.21.1",
"@esbuild/openbsd-x64": "0.21.1",
"@esbuild/sunos-x64": "0.21.1",
"@esbuild/win32-arm64": "0.21.1",
"@esbuild/win32-ia32": "0.21.1",
"@esbuild/win32-x64": "0.21.1"
"@esbuild/aix-ppc64": "0.23.0",
"@esbuild/android-arm": "0.23.0",
"@esbuild/android-arm64": "0.23.0",
"@esbuild/android-x64": "0.23.0",
"@esbuild/darwin-arm64": "0.23.0",
"@esbuild/darwin-x64": "0.23.0",
"@esbuild/freebsd-arm64": "0.23.0",
"@esbuild/freebsd-x64": "0.23.0",
"@esbuild/linux-arm": "0.23.0",
"@esbuild/linux-arm64": "0.23.0",
"@esbuild/linux-ia32": "0.23.0",
"@esbuild/linux-loong64": "0.23.0",
"@esbuild/linux-mips64el": "0.23.0",
"@esbuild/linux-ppc64": "0.23.0",
"@esbuild/linux-riscv64": "0.23.0",
"@esbuild/linux-s390x": "0.23.0",
"@esbuild/linux-x64": "0.23.0",
"@esbuild/netbsd-x64": "0.23.0",
"@esbuild/openbsd-arm64": "0.23.0",
"@esbuild/openbsd-x64": "0.23.0",
"@esbuild/sunos-x64": "0.23.0",
"@esbuild/win32-arm64": "0.23.0",
"@esbuild/win32-ia32": "0.23.0",
"@esbuild/win32-x64": "0.23.0"
}
},
"esbuild-wasm": {
@@ -25756,9 +25770,9 @@
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"requires": {
"to-regex-range": "^5.0.1"
}

View File

@@ -76,9 +76,9 @@
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.5.1",
"@fortawesome/fontawesome-svg-core": "~6.5.1",
"@fortawesome/free-solid-svg-icons": "~6.5.1",
"@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0",
@@ -92,7 +92,7 @@
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.21.1",
"esbuild": "^0.23.0",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
@@ -115,7 +115,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.10.0",
"cypress": "^13.13.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

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

View File

@@ -146,8 +146,9 @@ let routes: Routes = [
data: { preload: true },
},
{
path: 'tracker/:id',
component: TrackerComponent,
path: 'tracker',
data: { networkSpecific: true },
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
},
{
path: 'wallet',

View File

@@ -27,6 +27,7 @@ import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-st
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy';
import { ServicesApiServices } from './services/services-api.service';
import { DatePipe } from '@angular/common';
const providers = [
ElectrsApiService,
@@ -45,6 +46,7 @@ const providers = [
FiatShortenerPipe,
FiatCurrencyPipe,
CapAddressPipe,
DatePipe,
AppPreloadingStrategy,
ServicesApiServices,
PreloadService,

View File

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

View File

@@ -53,13 +53,26 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-10 -10 100 100" class="image">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-186.000000, -2316.000000)">
<g transform="translate(186.000000, 2316.000000)">
<rect id="" fill="#023D32" x="-10" y="-10" width="100" height="100" rx="8"></rect>
<path d="M61.6666667,9.16666667 L61.6666667,17.0041667 L46.2625,17.0041667 C46.2625,17.0041667 44.1666667,16.6666667 44.1666667,18.3333333 L44.1666667,25.8025 L61.6666667,25.8025 L61.6666667,34.7391667 L44.1666667,34.7391667 L44.1666667,70.5575 L31.7825,70.5575 L31.7825,35 L19.1666667,35 L19.1666667,25.595 L31.6666667,25.595 L31.6666667,17.5 C31.6666667,17.5 32.5,9.16666667 40.4166667,9.16666667 L61.6666667,9.16666667 Z" id="Fill-1" fill="#86E2A0"></path>
</g>
<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">
<defs>
<style>
.d {
fill: #fff;
}
.e {
fill: #ff8200;
}
</style>
</defs>
<g id="c" data-name="b">
<circle class="e" cx="24" cy="32" r="8" />
<circle class="e" cx="24" cy="56" r="8" />
<circle class="e" cx="8" cy="68" r="8" />
<g>
<circle class="d" cx="24" cy="8" r="8" />
<circle class="d" cx="8" cy="20" r="8" />
<circle class="d" cx="8" cy="44" r="8" />
</g>
</g>
</svg>
@@ -188,8 +201,8 @@
<div class="wrapper">
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@@ -200,8 +213,8 @@
<h3 i18n="about.sponsors.withHeart">Chad Sponsors</h3>
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -213,8 +226,8 @@
<h3 i18n="about.sponsors.withHeart">OG Sponsors ❤️</h3>
<div class="wrapper">
<ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
<a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<a *ngFor="let ogSponsor of ogs" [href]="'https://x.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-container>
</div>
@@ -259,22 +272,10 @@
<img class="image" src="/resources/profile/bisq_network.png" />
<span>Bisq</span>
</a>
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
<img class="image" src="/resources/profile/bluewallet.png" />
<span>BlueWallet</span>
</a>
<a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
<img class="image" src="/resources/profile/muun.png" />
<span>Muun</span>
</a>
<a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
<img class="image" src="/resources/profile/electrum.png" />
<span>Electrum</span>
</a>
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
<img class="image" src="/resources/profile/specter.png" />
<span>Specter</span>
</a>
<a href="https://github.com/sparrowwallet/sparrow" target="_blank" title="Sparrow Wallet">
<img class="image" src="/resources/profile/sparrow.png" />
<span>Sparrow</span>
@@ -283,21 +284,37 @@
<img class="image not-rounded" src="/resources/profile/phoenix.svg" />
<span>Phoenix</span>
</a>
<a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits">
<img class="image" src="/resources/profile/lnbits.svg" />
<span>LNBits</span>
<a href="http://github.com/COLDCARD" target="_blank" title="COLDCARD">
<img class="image coldcard" src="/resources/profile/coldcard.png" />
<span>COLDCARD</span>
</a>
<a href="https://github.com/layer2tech/mercury-wallet" target="_blank" title="Mercury Wallet">
<img class="image" src="/resources/profile/mercury.svg" />
<span>Mercury</span>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
<img class="image" src="/resources/profile/zeus.png" />
<span>ZEUS</span>
</a>
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
<span>Mutiny</span>
</a>
<a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet">
<img class="image" src="/resources/profile/blixt.png" />
<span>Blixt</span>
</a>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
<img class="image" src="/resources/profile/zeus.png" />
<span>ZEUS</span>
<a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck">
<img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span>
</a>
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
<img class="image" src="/resources/profile/bluewallet.png" />
<span>BlueWallet</span>
</a>
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
<img class="image" src="/resources/profile/boltz.svg" />
<span>Boltz</span>
</a>
<a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits">
<img class="image" src="/resources/profile/lnbits.svg" />
<span>LNBits</span>
</a>
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
<img class="image" src="/resources/profile/marina.svg" />
@@ -307,13 +324,9 @@
<img class="image" src="/resources/profile/schildbach.svg" />
<span>Schildbach</span>
</a>
<a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck">
<img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span>
</a>
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
<img class="image" src="/resources/profile/bitcoin-s.svg" />
<span>bitcoin-s</span>
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
<img class="image" src="/resources/profile/specter.png" />
<span>Specter</span>
</a>
<a href="https://github.com/EdgeApp" target="_blank" title="Edge">
<img class="image not-rounded" src="/resources/profile/edge.svg" />
@@ -323,13 +336,13 @@
<img class="image" src="/resources/profile/galoy.svg" />
<span>Galoy</span>
</a>
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
<img class="image" src="/resources/profile/boltz.svg" />
<span>Boltz</span>
<a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
<img class="image" src="/resources/profile/muun.png" />
<span>Muun</span>
</a>
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
<span>Mutiny</span>
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
<img class="image" src="/resources/profile/bitcoin-s.svg" />
<span>bitcoin-s</span>
</a>
</div>
</div>
@@ -354,8 +367,8 @@
<h3 i18n="about.translators">Project Translators</h3>
<div class="wrapper">
<ng-template ngFor let-translator [ngForOf]="translators">
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<a [href]="'https://x.com/' + translator.value" target="_blank" [title]="translator.key">
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -369,7 +382,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.regular">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>
@@ -381,7 +394,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.core">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name" [class]="'project-member-avatar'">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>
@@ -392,11 +405,11 @@
<div class="maintainers" id="project-maintainers">
<h3 i18n="about.maintainers">Project Maintainers</h3>
<div class="wrapper">
<a href="https://twitter.com/softsimon_" target="_blank" title="softsimon">
<a href="https://x.com/softsimon_" target="_blank" title="softsimon">
<img class="image" src="/resources/profile/softsimon.jpg" />
<span>softsimon</span>
</a>
<a href="https://twitter.com/wiz" target="_blank" title="wiz">
<a href="https://x.com/wiz" target="_blank" title="wiz">
<img class="image" src="/resources/profile/wiz.png" />
<span>wiz</span>
</a>

View File

@@ -10,9 +10,6 @@
margin: 25px;
line-height: 32px;
}
.unknown {
border: 1px solid #b4b4b4;
}
.image.not-rounded {
border-radius: 0;
@@ -159,6 +156,12 @@
}
img, svg {
margin: 40px 29px 10px;
&.image.coldcard {
border-radius: 0;
width: auto;
max-height: 50px;
margin: 40px 29px 14px 29px;
}
}
}
}

View File

@@ -1,77 +1,438 @@
<div class="container-md card w-100" style="padding: 1em; background: var(--box-bg)" id=acceleratePreviewAnchor>
@if (error) {
<div class="mt-2">
<app-mempool-error [error]="error"></app-mempool-error>
</div>
}
@else if (step === 'cta') {
<!-- Show A/B CTAs -->
<div class="row mb-1">
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
@if (accelerateError) {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Accelerate your Bitcoin transaction?</h1>
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
</div>
</div>
<div class="row text-center mt-1">
<div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center">
<span i18n="accelerator.error-failed-to-accelerate">We were not able to accelerate this transaction. Please try again later.</span>
</div>
</div>
</div>
<hr>
<div class="row mt-2 mb-2 text-center">
<div class="col-sm d-flex flex-column">
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="closeModal()" i18n="close">Close</button>
</div>
</div>
} @else if (step === 'quote') {
<div class="accelerate-cols">
<ng-container *ngIf="!isMobile">
<app-accelerate-fee-graph
[tx]="tx"
[estimate]="estimate"
[showEstimate]="hasAccessToBalanceMode"
[maxRateOptions]="maxRateOptions"
[maxRateIndex]="selectFeeRateIndex"
(setUserBid)="setUserBid($event)"
></app-accelerate-fee-graph>
</ng-container>
<ng-container *ngIf="estimate else loadingEstimate">
<div>
@if (showDetails) {
<h5 i18n="accelerator.your-transaction">Your transaction</h5>
<div class="row">
<div class="col">
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
<ng-container i18n="accelerator.plus-unconfirmed-ancestors">Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s)</ng-container>
</small>
<table class="table table-borderless table-border table-dark table-background table-accelerator">
<tbody>
<tr class="group-first">
<td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td style="text-align: end;" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
<td class="info" colspan=3>
<i><small i18n="accelerator.transaction-vbytes-size-description">Size in vbytes of this transaction (including unconfirmed ancestors)</small></i>
</td>
</tr>
<tr>
<td class="item" i18n="accelerator.in-band-fees">In-band fees</td>
<td style="text-align: end;">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
</td>
</tr>
<tr class="info group-last">
<td class="info" colspan=3>
<i><small i18n="accelerator.fees-already-paid-description">Fees already paid by this transaction (including unconfirmed ancestors)</small></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<br>
}
<h5 *ngIf="estimate?.pools?.length" i18n="accelerator.how-much-faster">How much faster?</h5>
<div class="row">
<div class="col">
<ng-container *ngIf="(etaInfo$ | async) as etaInfo; else loadingEstimate">
<small class="form-text checkout-text mb-2"><ng-container *ngTemplateOutlet="prioritizedBy; context: {$implicit:etaInfo.hashratePercentage}"></ng-container></small>
<small class="form-text checkout-text mb-2" i18n="accelerator.time-estimate-description">This will reduce your expected waiting time until the first confirmation to <strong><app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></strong></small>
</ng-container>
</div>
<div class="col pie">
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group">
<div class="fee-card">
<div class="d-flex mb-0">
<ng-container *ngFor="let option of maxRateOptions">
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
<span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
</button>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<h5 i18n="accelerator.summary-title">Summary</h5>
<div class="row">
<div class="col">
<table class="table table-borderless table-border table-dark table-background table-accelerator">
<tbody>
<!-- ESTIMATED FEE -->
<ng-container *ngIf="showDetails">
@if (hasAccessToBalanceMode) {
<tr class="group-first">
<td class="item" i18n="accelerator.next-block-rate">Next block market rate</td>
<td class="amt" style="font-size: 16px">
{{ estimate.targetFeeRate | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small i18n="accelerator.estimated-extra-fee-required">Estimated extra fee required</small></i>
</td>
<td class="amt">
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
}
@else {
<!-- TARGET FEE -->
<tr class="group-first">
<td class="item" i18n="accelerator.target-rate">Target rate</td>
<td class="amt" style="font-size: 16px">
{{ maxRateOptions[selectFeeRateIndex].rate | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small i18n="accelerator.extra-fee-required">Extra fee required</small></i>
</td>
<td class="amt">
{{ maxRateOptions[selectFeeRateIndex].fee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="maxRateOptions[selectFeeRateIndex].fee"></app-fiat></span>
</td>
</tr>
}
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td>
</tr>
<tr class="info" [class.group-last]="!estimate.vsizeFee" [class.dashed-bottom]="!estimate.vsizeFee">
<td class="info">
<i><small i18n="accelerator.service-fee">Accelerator Service Fee</small></i>
</td>
<td class="amt">
+{{ estimate.mempoolBaseFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last dashed-bottom" *ngIf="estimate.vsizeFee">
<td class="info">
<i><small i18n="accelerator.tx-size-surcharge">Transaction Size Surcharge</small></i>
</td>
<td class="amt">
+{{ estimate.vsizeFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container *ngIf="hasAccessToBalanceMode">
<tr class="group-first">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0" i18n="accelerator.estimated-cost">Estimated acceleration cost</b> ~{{ estimate.targetFeeRate | number : '1.0-0' }} sat/vB
</td>
<td class="amt">
<span style="background-color: #5E35B1" class="p-1 pl-0">
{{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- MAX COST -->
<ng-container>
<tr class="group-first group-last">
<td class="item">
@if (hasAccessToBalanceMode) {
<b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b>
} @else {
<b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.cost">Acceleration cost</b>
}
</td>
<td class="amt">
<span style="background-color: var(--primary)" class="p-1 pl-0">
{{ cost | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="cost" [colorClass]="hasAccessToBalanceMode && estimate.userBalance < cost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="hasAccessToBalanceMode && estimate.userBalance < cost">
<tr class="group-first group-last dashed-top">
<td class="item" i18n="accelerator.available-balance">Available balance</td>
<td class="amt">
{{ estimate.userBalance | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < cost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td colspan="2">
<div class="d-flex">
<ng-container *ngTemplateOutlet="accelerateButton"></ng-container>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</ng-container>
</div>
<hr>
<div class="row mt-2 mb-2 text-center">
<div class="col-sm d-flex flex-column">
<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>
<form>
<div class="row">
<div class="col-sm">
<div class="form-group form-check mb-2">
<input type="radio" class="form-check-input" id="accelerate" name="accelerate" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="accelerate">
<span class="font-weight-bold">Accelerate</span>
<span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected within ~30 minutes<br>
@if (!calculating) {
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span>)
} @else {
<span class="estimating">Calculating cost...</span>
}
</span>
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group form-check mb-2">
<input type="radio" class="form-check-input" id="wait" name="accelerate" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="wait">
<span class="font-weight-bold">Wait</span>
@if (eta) {
<span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected <app-time kind="within" [time]="eta" [fastRender]="false" [fixedRender]="true"></app-time></span>
} @else {
<span style="color: rgb(186, 186, 186); font-size: 14px;">
<span>Settlement expected within several hours</span>
</span>
}
</label>
</div>
</div>
</div>
<div class="row mt-2 mb-2" [style]="(choosenOption === 'wait' || calculating) ? 'opacity: 0.25; pointer-events: none' : ''">
<div class="col-sm d-flex flex-row justify-content-center">
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" style="width: 200px" (click)="enableCheckoutPage()">
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
<span>Accelerate</span>
</button>
</div>
</div>
</form>
<ng-template #loadingEstimate>
<div class="skeleton-loader"></div>
<br>
</ng-template>
}
@else if (step === 'checkout') {
@else if (step === 'summary') {
<ng-container *ngIf="estimate && (etaInfo$ | async) as etaInfo; else loadingSummary">
<!-- Show A/B CTAs -->
@if (!noCTA) {
<div class="row mb-1">
<div class="col-sm">
<h1 style="font-size: larger;"><ng-content select="[slot='cta-title']"></ng-content><span class="default-slot" i18n="accelerator.accelerate-your-transaction">Accelerate your Bitcoin transaction?</span></h1>
</div>
</div>
}
@if (!advancedEnabled) {
<form>
<div class="row">
<div class="col-md">
<div class="form-group form-check mb-2">
<input type="radio" [checked]="selectedOption === 'wait'" class="form-check-input" id="wait" name="accel" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="wait">
<span class="font-weight-bold" i18n="accelerator.wait">Wait</span>
@if (eta.blocks < 7) {
<span class="checkout-text"><ng-container i18n="accelerator.confirmation-expected">Confirmation expected</ng-container>&nbsp;<app-time kind="within" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time></span>
} @else {
<span class="checkout-text">
<span i18n="accelerator.confirmation-not-expected-soon">Confirmation not expected any time soon</span>
</span>
}
</label>
</div>
<div class="form-group form-check mb-2">
<input type="radio" [checked]="selectedOption === 'accel'" class="form-check-input" id="accel" name="accel" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="accel">
<ng-container *ngTemplateOutlet="accelerateOption; context: {etaInfo}"></ng-container>
</label>
</div>
</div>
</div>
<div class="row mt-2 mb-2">
<div class="col-sm d-flex flex-row justify-content-center">
<ng-container *ngTemplateOutlet="accelerateButton"></ng-container>
</div>
</div>
</form>
} @else {
<div>
<div class="row summary-row">
<div>
<div class="mb-2">
<div class="d-flex flex-column" for="accel">
<ng-container *ngTemplateOutlet="accelerateOption; context: {etaInfo}"></ng-container>
</div>
</div>
</div>
<div class="pie d-none d-lg-flex">
<small class="form-text checkout-text mb-2"><ng-container *ngTemplateOutlet="prioritizedBy; context: {$implicit:etaInfo.hashratePercentage}"></ng-container></small>
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
</div>
<ng-container *ngTemplateOutlet="accelerateButton"></ng-container>
</div>
</div>
}
</ng-container>
<ng-template #loadingSummary>
<div class="row">
<div class="col-md">
<div class="d-flex flex-row justify-content-center align-items-center">
<div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
</ng-template>
} @else if (step === 'checkout') {
<ng-container *ngIf="estimate && (etaInfo$ | async) as etaInfo; else loadingCheckout">
<div class="row">
<div class="col-md">
<div class="d-flex flex-column">
<span><ng-container *ngTemplateOutlet="accelerateTo; context: {$implicit:(userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize}"></ng-container></span>
<span class="checkout-text">
@if (!calculating) {
<ng-container i18n="accelerator.for-an-additional-cost">For an additional</ng-container> <app-fiat [value]="cost"></app-fiat> (<span><small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span>)
} @else {
<span class="estimating">Calculating cost...</span>
}
</span>
<span class="checkout-text" *ngIf="(etaInfo$ | async) as etaInfo">
<ng-container i18n="accelerator.reducing-expected-confirmation-time">Reducing expected confirmation time to <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></ng-container>
</span>
</div>
</div>
<div class="col-md pie d-none d-md-flex" *ngIf="!forceMobile">
<small class="form-text checkout-text mb-2" *ngIf="(etaInfo$ | async) as etaInfo"><ng-container *ngTemplateOutlet="prioritizedBy; context: {$implicit:etaInfo.hashratePercentage}"></ng-container></small>
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
</div>
</div>
<div class="payment-area mt-2 p-2" style="font-size: 14px;">
<div class="row text-center justify-content-center mx-2">
<p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
</div>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
<div class="row">
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p>
<div class="d-flex justify-content-center" [class.grayOut]="!canPayWithBalance || quoteError || accelerateError || showSuccess">
<ng-container *ngTemplateOutlet="accountPayButton"></ng-container>
</div>
</div>
</div>
} @else {
<div class="row">
@if (canPayWithBitcoin) {
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
@if (invoice) {
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span></p>
<app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
} @else if (btcpayInvoiceFailed) {
<p i18n="accelerator.failed-to-load-invoice">Failed to load invoice</p>
<div class="d-flex flex-column align-items-center justify-content-center" style="width: 100%; height: 292px;">
<fa-icon style="font-size: 24px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
</div>
} @else {
<p i18n="accelerator.loading-invoice">Loading invoice...</p>
<div class="d-flex align-items-center justify-content-center" style="width: 100%; height: 292px;">
<div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div>
@if (canPayWithCashapp) {
<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) {
<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')">
</div>
}
</div>
}
</div>
</ng-container>
<ng-template #loadingCheckout>
<div class="row">
<div class="col-md">
<div class="d-flex flex-row justify-content-center align-items-center">
<div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
</ng-template>
<hr>
<div class="row mt-2 mb-2 text-center">
<div class="col-sm d-flex flex-column">
<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') {
<!-- Show checkout page -->
<div class="row mb-md-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Confirm your payment</h1>
<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>
</div>
<div class="row text-center">
<div class="col-sm">
<div class="form-group w-100" style="font-size: 14px">
Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + txid" target="_blank">{{ txid.substr(0, 10) }}..{{ txid.substr(-10) }}</a>
<ng-container i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></ng-container>
</div>
</div>
</div>
@@ -80,11 +441,11 @@
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
<span><u><strong>Total additional cost</strong></u><br>
<span><u><strong i18n="accelerator.total-additional-cost">Total additional cost</strong></u><br>
<span style="font-size: 16px" class="d-block mt-2">
Pay
<ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>
<strong><app-fiat [value]="cost"></app-fiat></strong>
with
<ng-container i18n="accelerator.pay-with">with</ng-container>
</span>
</span>
</div>
@@ -98,7 +459,7 @@
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
@if (loadingCashapp) {
<div display="d-flex flex-row justify-content-center">
<span>Loading payment method...</span>
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
@@ -109,16 +470,14 @@
<hr>
<div class="row mt-2 mb-2 text-center">
<div class="col-sm d-flex flex-column">
<small>Changed your mind?</small>
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="step = 'cta'">Go Back</button>
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('checkout')" i18n="go-back">Go back</button>
</div>
</div>
}
@else if (step === 'processing') {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Confirm your payment</h1>
<h1 style="font-size: larger;"><ng-content select="[slot='processing-title']"></ng-content><span class="default-slot" i18n="accelerator.confirming-your-payment">Confirming your payment</span></h1>
</div>
</div>
@@ -128,12 +487,94 @@
<!-- Processing payment -->
<div id="cash-app-pay" class="d-inline-block" [style]="'opacity: 0; width: 0px; height: 0px; pointer-events: none;'"></div>
<div display="d-flex flex-row justify-content-center">
<span>We are processing your payment...</span>
<span i18n="accelerator.payment-processing">We are processing your payment...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
</div>
}
@else if (step === 'paid') {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;"><ng-content select="[slot='accelerating-title']"></ng-content><span class="default-slot" i18n="accelerator.accelerating-your-transaction">Accelerating your transaction</span></h1>
</div>
</div>
<div class="row text-center mt-1">
<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) {
<span i18n="accelerator.confirming-acceleration-with-miners">...sorry, this is taking longer than expected...</span>
}
<div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
} @else if (step === 'success') {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;"><ng-content select="[slot='accelerated-title']"></ng-content><span class="default-slot" i18n="accelerator.success-message">Your transaction is being accelerated!</span></h1>
</div>
</div>
<div class="row text-center mt-1">
<div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center">
<span i18n="accelerator.confirmed-acceleration-with-miners">Your transaction has been accepted for acceleration by our mining pool partners.</span>
</div>
</div>
</div>
<hr>
<div class="row mt-2 mb-2 text-center">
<div class="col-sm d-flex flex-column">
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="closeModal()" i18n="close">Close</button>
</div>
</div>
}
</div>
<ng-template #accelerateOption let-etaInfo="etaInfo">
<span><ng-container *ngTemplateOutlet="accelerateTo; context: {$implicit:(userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize}"></ng-container> <ng-container *ngTemplateOutlet="customizeButton"></ng-container></span>
<span class="checkout-text"><ng-container i18n="accelerator.confirmation-expected">Confirmation expected</ng-container>&nbsp;<app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time><br>
@if (!calculating) {
<app-fiat [value]="cost"></app-fiat> (<span><small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span>)
} @else {
<span class="estimating" i18n="accelerator.calculating-cost">Calculating cost...</span>
}
</span>
</ng-template>
<ng-template #customizeButton>
<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 #accelerateButton>
<div class="position-relative">
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || quoteError || cantPayReason || calculating || (!advancedEnabled && selectedOption !== 'accel')" style="width: 200px" (click)="moveToStep('checkout')">
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
<span i18n="transaction.accelerate|Accelerate button label">Accelerate</span>
</button>
@if (quoteError || cantPayReason) {
<div class="btn-error-wrapper"><span class="btn-error"><app-mempool-error [error]="quoteError || cantPayReason" [textOnly]="true" alertClass=""></app-mempool-error></span></div>
}
</div>
</ng-template>
<ng-template #accountPayButton>
@if (hasAccessToBalanceMode) {
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || calculating" style="width: 200px" (click)="accelerateWithMempoolAccount()">
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
<span i18n="transaction.pay|Pay button label">Pay</span>
</button>
} @else {
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center disabled" style="width: 200px">
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
<span>Coming soon</span>
</button>
}
</ng-template>
<ng-template #prioritizedBy let-i i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to <strong>{{ i | number : '1.1-1' }}%</strong> of miners.</ng-template>

View File

@@ -7,3 +7,199 @@
.estimating {
color: var(--green)
}
.paymentMethod {
padding: 10px;
background-color: var(--secondary);
border-radius: 15px;
border: 2px solid var(--bg);
cursor: pointer;
}
.default-slot:not(:only-child) {
display: none;
}
.pie {
display: flex;
align-items: center;
max-width: 330px;
}
.fee-card {
padding: 15px;
background-color: var(--bg);
.feerate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.rate {
font-size: 0.9em;
.symbol {
color: white;
}
}
}
}
.btn-border {
border: solid 1px black;
background-color: #0c4a87;
}
.feerate.active {
background-color: var(--primary) !important;
opacity: 1;
border: 1px solid #007fff !important;
}
.feerate:focus {
box-shadow: none !important;
}
.grayOut {
opacity: 0.5;
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
.table-toggle {
width: 100%;
margin-top: 0.5em;
}
.tab {
&:first-child {
margin-right: 1px;
}
border: solid 1px black;
border-bottom: none;
background-color: #323655;
border-top-left-radius: 10px !important;
border-top-right-radius: 10px !important;
}
.tab.active {
background-color: #5d659d !important;
opacity: 1;
}
.tab:focus {
box-shadow: none !important;
}
.table-accelerator {
tr {
td {
padding-top: 0;
padding-bottom: 0;
vertical-align: baseline;
}
&.group-first {
td {
padding-top: 0.75rem;
}
}
&.group-last, &:last-child {
td {
padding-bottom: 0.75rem;
}
}
&.dashed-top {
border-top: 1px dashed grey;
}
&.dashed-bottom {
border-bottom: 1px dashed grey
}
}
td {
&:first-child {
width: 100vw;
}
&.info {
color: #6c757d;
white-space: initial;
}
&.amt {
text-align: right;
padding-right: 0.2em;
}
&.units {
padding-left: 0.2em;
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
.accelerate-cols {
display: flex;
flex-direction: row;
align-items: stretch;
margin-top: 1em;
}
.payment-area {
background: var(--bg);
}
.col.pie {
flex-grow: 0;
padding: 0 1em;
position: relative;
top: -15px;
}
.item {
white-space: initial;
}
.table-background {
background-color: var(--bg);
}
.checkout-text {
color: rgb(186, 186, 186);
font-size: 14px;
}
.btn-accelerate {
background-color: var(--tertiary);
}
.btn-small-height {
line-height: 1;
}
.summary-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 2em;
flex-wrap: wrap;
@media (max-width: 640px) {
flex-direction: column;
}
}
.btn-error {
position: absolute;
right: 0;
font-size: 12px;
color: var(--red);
text-align: center;
width: 200px;
white-space: normal;
}
.btn-error-wrapper {
height: 26px;
}

View File

@@ -1,9 +1,52 @@
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges } from '@angular/core';
import { Subscription, tap, of, catchError } from 'rxjs';
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 } from '../../shared/common.utils';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ETA, EtaService } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { MiningStats } from '../../services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp';
export type AccelerationEstimate = {
hasAccess: boolean;
txSummary: TxSummary;
nextBlockFee: number;
targetFeeRate: number;
userBalance: number;
enoughBalance: boolean;
cost: number;
mempoolBaseFee: number;
vsizeFee: number;
pools: number[];
availablePaymentMethods: {[method: string]: {min: number, max: number}};
unavailable?: boolean;
options: { // recommended bid options
fee: number; // recommended userBid in sats
}[];
}
export type TxSummary = {
txid: string; // txid of the current transaction
effectiveVsize: number; // Total vsize of the dependency tree
effectiveFee: number; // Total fee of the dependency tree in sats
ancestorCount: number; // Number of ancestors
}
export interface RateOption {
fee: number;
rate: number;
index: number;
}
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';
@Component({
selector: 'app-accelerate-checkout',
@@ -11,46 +54,108 @@ import { AudioService } from '../../services/audio.service';
styleUrls: ['./accelerate-checkout.component.scss']
})
export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() eta: number | null = null;
@Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad';
@Input() tx: Transaction;
@Input() accelerating: boolean = false;
@Input() miningStats: MiningStats;
@Input() eta: ETA;
@Input() scrollEvent: boolean;
@Output() close = new EventEmitter<null>();
@Input() cashappEnabled: boolean = true;
@Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false;
@Input() noCTA: boolean = false;
@Output() unavailable = new EventEmitter<boolean>();
@Output() completed = new EventEmitter<boolean>();
@Output() hasDetails = new EventEmitter<boolean>();
@Output() changeMode = new EventEmitter<boolean>();
calculating = true;
choosenOption: 'wait' | 'accelerate' = 'wait';
error = '';
selectedOption: 'wait' | 'accel';
cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data
accelerateError = ''; // error executing acceleration
btcpayInvoiceFailed = false;
timePaid: number = 0; // time acceleration requested
math = Math;
isMobile: boolean = window.innerWidth <= 767.98;
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;
estimateSubscription: Subscription;
estimate: AccelerationEstimate;
maxBidBoost: number; // sats
cost: number; // sats
etaInfo$: Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }>;
showSuccess = false;
hasAncestors: boolean = false;
minExtraCost = 0;
minBidAllowed = 0;
maxBidAllowed = 0;
defaultBid = 0;
userBid = 0;
selectFeeRateIndex = 1;
maxRateOptions: RateOption[] = [];
// square
loadingCashapp = false;
cashappError = false;
cashappSubmit: any;
payments: any;
cashAppPay: any;
cashAppSubscription: Subscription;
conversionsSubscription: Subscription;
step: 'cta' | 'checkout' | 'processing' = 'cta';
conversions: any;
// btcpay
loadingBtcpayInvoice = false;
invoice = undefined;
constructor(
public stateService: StateService,
private servicesApiService: ServicesApiServices,
private stateService: StateService,
private etaService: EtaService,
private audioService: AudioService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService,
) {
this.accelerationUUID = window.crypto.randomUUID();
}
ngOnInit() {
this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => {
if (this.auth?.user?.userId !== auth?.user?.userId) {
this.auth = auth;
this.estimate = null;
this.quoteError = null;
this.accelerateError = null;
this.timePaid = 0;
this.btcpayInvoiceFailed = false;
this.moveToStep('summary');
} else {
this.auth = auth;
}
});
this.authService.refreshAuth$().subscribe();
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.moveToStep('processing');
this.insertSquare();
this.setupSquare();
this.step = 'processing';
} else {
this.moveToStep('summary');
}
this.servicesApiService.setupSquare$().subscribe(ids => {
@@ -58,33 +163,80 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
if (this.step === 'cta') {
this.estimate();
}
});
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
}
);
}
ngOnDestroy() {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
if (this.authSubscription$) {
this.authSubscription$.unsubscribe();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
if (changes.scrollEvent && this.scrollEvent) {
this.scrollToElement('acceleratePreviewAnchor', 'start');
}
if (changes.accelerating) {
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
this.moveToStep('success');
}
}
}
moveToStep(step: CheckoutStep) {
this._step = step;
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
}
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
this.fetchEstimate();
}
if (this._step === 'checkout') {
this.enterpriseService.goal(8);
}
if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.btcpayInvoiceFailed = false;
this.loadingBtcpayInvoice = true;
this.invoice = null;
this.requestBTCPayInvoice();
} else if (this._step === 'cashapp' && this.cashappEnabled) {
this.loadingCashapp = true;
this.insertSquare();
this.setupSquare();
} else if (this._step === 'paid') {
this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => {
if (this.step === 'paid') {
this.accelerateError = 'internal_server_error';
}
}, 120000)
}
this.hasDetails.emit(this._step === 'quote');
}
closeModal(): void {
this.completed.emit(true);
this.moveToStep('summary');
}
/**
* Scroll to element id with or without setTimeout
*/
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void {
setTimeout(() => {
this.scrollToPreview(id, position);
}, 1000);
this.scrollToElement(id, position);
}, timeout);
}
scrollToPreview(id: string, position: ScrollLogicalPosition) {
scrollToElement(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
this.cd.markForCheck();
@@ -99,37 +251,136 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
/**
* Accelerator
*/
estimate() {
fetchEstimate() {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
this.calculating = true;
this.estimateSubscription = this.servicesApiService.estimate$(this.txid).pipe(
this.quoteError = null;
this.accelerateError = null;
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
tap((response) => {
this.calculating = false;
if (response.status === 204) {
this.error = `cannot_accelerate_tx`;
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
this.unavailable.emit(true);
}
} else {
const estimation = response.body;
if (!estimation) {
this.error = `cannot_accelerate_tx`;
this.estimate = response.body;
if (!this.estimate) {
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
this.unavailable.emit(true);
}
return;
}
// Make min extra fee at least 50% of the current tx fee
const minExtraBoost = nextRoundNumber(Math.max(estimation.cost * 2, estimation.txSummary.effectiveFee));
const DEFAULT_BID_RATIO = 2;
this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO;
this.cost = this.maxBidBoost + estimation.mempoolBaseFee + estimation.vsizeFee;
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.quoteError = `not_enough_balance`;
}
}
if (this.estimate.unavailable) {
this.quoteError = `temporarily_unavailable`;
}
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate, this.miningStats);
this.maxRateOptions = this.estimate.options.map((option, index) => ({
fee: option.fee,
rate: (this.estimate.txSummary.effectiveFee + option.fee) / this.estimate.txSummary.effectiveVsize,
index
}));
this.defaultBid = this.maxRateOptions[1].fee;
this.userBid = this.defaultBid;
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.validateChoice();
if (!this.couldPay) {
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
this.unavailable.emit(true);
}
return;
}
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
this.loadingBtcpayInvoice = true;
this.requestBTCPayInvoice();
}
this.calculating = false;
this.cd.markForCheck();
}
}),
catchError((response) => {
this.error = `cannot_accelerate_tx`;
this.estimate = undefined;
this.quoteError = `cannot_accelerate_tx`;
this.estimateSubscription.unsubscribe();
if (this.step === 'summary') {
this.unavailable.emit(true);
} else {
this.accelerateError = 'cannot_accelerate_tx';
}
return of(null);
})
).subscribe();
}
validateChoice(): void {
if (!this.canPay) {
if (this.estimate?.availablePaymentMethods?.balance) {
if (this.cost >= this.estimate?.userBalance) {
this.cantPayReason = 'not_enough_balance';
}
} else {
this.cantPayReason = 'cannot_accelerate_tx';
}
} else {
this.cantPayReason = '';
}
}
/**
* User changed his bid
*/
setUserBid({ fee, index }: { fee: number, index: number}): void {
if (this.estimate) {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
}
}
/**
* Account-based acceleration request
*/
accelerateWithMempoolAccount(): void {
if (!this.canPay || this.calculating) {
return;
}
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid,
this.userBid,
this.accelerationUUID
).subscribe({
next: () => {
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.estimateSubscription.unsubscribe();
this.moveToStep('paid')
},
error: (response) => {
this.accelerateError = response.error;
}
});
}
/**
* Square
*/
@@ -173,6 +424,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
await this.requestCashAppPayment();
} catch (e) {
console.debug('Error loading Square Payments', e);
this.cashappError = true;
return;
}
}
@@ -186,6 +438,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
@@ -199,17 +452,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
amount: costUSD.toString(),
label: 'Total',
pending: true,
productUrl: `${redirectHostname}/tracker/${this.txid}`,
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`,
},
button: { shape: 'semiround', size: 'small', theme: 'light'}
});
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
redirectURL: `${redirectHostname}/tracker/${this.txid}`,
referenceId: `accelerator-${this.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
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'}
});
if (this.step === 'checkout') {
if (this.step === 'cashapp') {
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
}
this.loadingCashapp = false;
@@ -218,10 +471,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.addEventListener('ontokenization', function (event) {
const { tokenResult, error } = event.detail;
if (error) {
this.error = error;
this.accelerateError = error;
} else if (tokenResult.status === 'OK') {
that.servicesApiService.accelerateWithCashApp$(
that.txid,
that.tx.txid,
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
@@ -233,7 +486,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
that.cashAppPay.destroy();
}
setTimeout(() => {
that.closeModal();
this.moveToStep('paid');
if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
@@ -241,10 +494,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000);
},
error: (response) => {
if (response.status === 403 && response.error === 'not_available') {
that.error = 'waitlisted';
} else {
that.error = response.error;
that.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);
@@ -259,19 +510,115 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
);
}
/**
* BTCPay
*/
async requestBTCPayInvoice() {
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
}),
catchError(error => {
console.log(error);
this.btcpayInvoiceFailed = true;
return of(null);
})
).subscribe((invoice) => {
this.invoice = invoice;
this.cd.markForCheck();
});
}
bitcoinPaymentCompleted(): void {
this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe();
this.moveToStep('paid')
}
isLoggedIn(): boolean {
return this.auth !== null;
}
/**
* UI events
*/
enableCheckoutPage() {
this.step = 'checkout';
this.loadingCashapp = true;
this.insertSquare();
this.setupSquare();
}
selectedOptionChanged(event) {
this.choosenOption = event.target.id;
this.selectedOption = event.target.id;
}
closeModal(): void {
this.close.emit();
get step() {
return this._step;
}
get paymentMethods() {
return Object.keys(this.estimate?.availablePaymentMethods || {});
}
get couldPayWithBitcoin() {
return !!this.estimate?.availablePaymentMethods?.bitcoin;
}
get couldPayWithCashapp() {
if (!this.cashappEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.cashapp;
}
get couldPayWithBalance() {
if (!this.hasAccessToBalanceMode) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.balance;
}
get couldPay() {
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp;
}
get canPayWithBitcoin() {
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
}
get canPayWithCashapp() {
if (!this.cashappEnabled || !this.conversions) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.cashapp;
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() {
if (!this.hasAccessToBalanceMode) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.balance;
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 hasAccessToBalanceMode() {
return this.isLoggedIn() && this.estimate?.hasAccess;
}
get timeSincePaid(): number {
return Date.now() - this.timePaid;
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}

View File

@@ -1,4 +1,4 @@
<div class="fee-graph" *ngIf="tx && estimate">
<div class="fee-graph" *ngIf="tx && estimate" #feeGraph>
<div class="column">
<ng-container *ngFor="let bar of bars">
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
@@ -12,7 +12,7 @@
</p>
</div>
<div class="spacer"></div>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
<div class="spacer"></div>
<div class="spacer"></div>
</div>

View File

@@ -4,7 +4,6 @@
width: 120px;
margin-left: 4em;
margin-right: 1.5em;
padding-bottom: 63px;
.column {
width: 100%;

View File

@@ -0,0 +1,152 @@
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '../../interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
interface GraphBar {
rate: number;
style?: Record<string,string>;
class: 'tx' | 'target' | 'max';
label: string;
active?: boolean;
rateIndex?: number;
fee?: number;
height?: number;
}
@Component({
selector: 'app-accelerate-fee-graph',
templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'],
})
export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
@Input() tx: Transaction;
@Input() estimate: AccelerationEstimate;
@Input() showEstimate = false;
@Input() maxRateOptions: RateOption[] = [];
@Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
@ViewChild('feeGraph')
container: ElementRef<HTMLDivElement>;
height: number;
observer: ResizeObserver;
stopResizeLoop = false;
bars: GraphBar[] = [];
tooltipPosition = { x: 0, y: 0 };
constructor(
private cd: ChangeDetectorRef,
) {}
ngOnInit(): void {
this.initGraph();
}
ngAfterViewInit(): void {
if (ResizeObserver) {
this.observer = new ResizeObserver(entries => {
for (const entry of entries) {
this.height = entry.contentRect.height;
this.initGraph();
}
});
this.observer.observe(this.container.nativeElement);
} else {
this.startResizeFallbackLoop();
}
}
ngOnChanges(): void {
this.initGraph();
}
initGraph(): void {
if (!this.tx || !this.estimate) {
return;
}
const hasNextBlockRate = (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee);
const numBars = hasNextBlockRate ? 4 : 3;
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
let baseHeight = Math.max(this.height - (numBars * 30), this.height * (baseRate / maxRate));
const bars: GraphBar[] = [];
let lastHeight = 0;
if (hasNextBlockRate) {
lastHeight = Math.max(lastHeight + 30, (this.height * ((this.estimate.targetFeeRate - baseRate) / maxRate)));
bars.push({
rate: this.estimate.targetFeeRate,
height: lastHeight,
class: 'target',
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
});
}
this.maxRateOptions.forEach((option, index) => {
lastHeight = Math.max(lastHeight + 30, (this.height * ((option.rate - baseRate) / maxRate)));
bars.push({
rate: option.rate,
height: lastHeight,
class: 'max',
label: this.showEstimate ? $localize`maximum` : $localize`accelerated`,
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
})
})
bars.reverse();
baseHeight = this.height - lastHeight;
for (const bar of bars) {
bar.style = this.getStyle(bar.height, baseHeight);
}
bars.push({
rate: baseRate,
style: this.getStyle(baseHeight, 0),
height: baseHeight,
class: 'tx',
label: '',
fee: this.estimate.txSummary.effectiveFee,
});
this.bars = bars;
this.cd.detectChanges();
}
getStyle(height: number, base: number): Record<string,string> {
return {
height: `${height}px`,
bottom: base ? `${base}px` : '0',
}
}
onClick(event, bar): void {
if (bar.rateIndex != null) {
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
}
startResizeFallbackLoop(): void {
if (this.stopResizeLoop) {
return;
}
requestAnimationFrame(() => {
this.height = this.container?.nativeElement?.clientHeight || 0;
this.initGraph();
this.startResizeFallbackLoop();
});
}
ngOnDestroy(): void {
this.stopResizeLoop = true;
this.observer.disconnect();
}
}

View File

@@ -1,98 +0,0 @@
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router';
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { AccelerationEstimate, RateOption } from './accelerate-preview.component';
interface GraphBar {
rate: number;
style: any;
class: 'tx' | 'target' | 'max';
label: string;
active?: boolean;
rateIndex?: number;
fee?: number;
}
@Component({
selector: 'app-accelerate-fee-graph',
templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'],
})
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
@Input() tx: Transaction;
@Input() estimate: AccelerationEstimate;
@Input() maxRateOptions: RateOption[] = [];
@Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
bars: GraphBar[] = [];
tooltipPosition = { x: 0, y: 0 };
ngOnInit(): void {
this.initGraph();
}
ngOnChanges(): void {
this.initGraph();
}
initGraph(): void {
if (!this.tx || !this.estimate) {
return;
}
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
const baseHeight = baseRate / maxRate;
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
return {
rate: option.rate,
style: this.getStyle(option.rate, maxRate, baseHeight),
class: 'max',
label: $localize`maximum`,
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
}
});
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
bars.push({
rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
class: 'target',
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
});
}
bars.push({
rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0),
class: 'tx',
label: '',
fee: this.estimate.txSummary.effectiveFee,
});
this.bars = bars;
}
getStyle(rate, maxRate, base) {
const top = (rate / maxRate);
return {
height: `${(top - base) * 100}%`,
bottom: base ? `${base * 100}%` : '0',
}
}
onClick(event, bar): void {
if (bar.rateIndex != null) {
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
}
}

View File

@@ -1,250 +0,0 @@
<span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="showSuccess">
<div class="col">
<div class="alert alert-success">
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
</div>
</div>
</div>
<span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span>
<div class="row" *ngIf="error">
<div class="col">
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error>
</div>
</div>
<div class="accelerate-cols">
<ng-container *ngIf="!isMobile">
<app-accelerate-fee-graph
[tx]="tx"
[estimate]="estimate"
[maxRateOptions]="maxRateOptions"
[maxRateIndex]="selectFeeRateIndex"
(setUserBid)="setUserBid($event)"
></app-accelerate-fee-graph>
</ng-container>
<ng-container *ngIf="estimate else loadingEstimate">
<div [class]="{estimateDisabled: error || showSuccess }">
<div *ngIf="user && !estimate.hasAccess">
<div class="alert alert-mempool">You are currently on the waitlist</div>
</div>
<h5 i18n="accelerator.your-transaction">Your transaction</h5>
<div class="row">
<div class="col">
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
<ng-container i18n="accelerator.plus-unconfirmed-ancestors">Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s)</ng-container>
</small>
<table class="table table-borderless table-border table-dark table-background table-accelerator">
<tbody>
<tr class="group-first">
<td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td style="text-align: end;" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
<td class="info" colspan=3>
<i><small i18n="accelerator.transaction-vbytes-size-description">Size in vbytes of this transaction (including unconfirmed ancestors)</small></i>
</td>
</tr>
<tr>
<td class="item" i18n="accelerator.in-band-fees">In-band fees</td>
<td style="text-align: end;">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
</td>
</tr>
<tr class="info group-last">
<td class="info" colspan=3>
<i><small i18n="accelerator.fees-already-paid-description">Fees already paid by this transaction (including unconfirmed ancestors)</small></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<br>
<h5 i18n="accelerator.pay-how-much">How much more are you willing to pay?</h5>
<div class="row">
<div class="col">
<small class="form-text text-muted mb-2" i18n="accelerator.transaction-fee-description">Choose the maximum extra transaction fee you're willing to pay to get into the next block.</small>
<div class="form-group">
<div class="fee-card">
<div class="d-flex mb-0">
<ng-container *ngFor="let option of maxRateOptions">
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
<span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
</button>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<h5>Acceleration summary</h5>
<div class="row mb-3">
<div class="col">
<table class="table table-borderless table-border table-dark table-background table-accelerator">
<tbody>
<!-- ESTIMATED FEE -->
<ng-container>
<tr class="group-first">
<td class="item" i18n="accelerator.next-block-rate">Next block market rate</td>
<td class="amt" style="font-size: 16px">
{{ estimate.targetFeeRate | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small i18n="accelerator.estimated-extra-fee-required">Estimated extra fee required</small></i>
</td>
<td class="amt">
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td>
</tr>
<tr class="info">
<td class="info">
<i><small i18n="accelerator.service-fee">Accelerator Service Fee</small></i>
</td>
<td class="amt">
+{{ estimate.mempoolBaseFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<i><small i18n="accelerator.tx-size-surcharge">Transaction Size Surcharge</small></i>
</td>
<td class="amt">
+{{ estimate.vsizeFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container>
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0" i18n="accelerator.estimated-cost">Estimated acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #5E35B1" class="p-1 pl-0">
{{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info" colspan=3>
<i><small><ng-container *ngTemplateOutlet="acceleratedTo; context: {$implicit: estimate.targetFeeRate }"></ng-container></small></i>
</td>
</tr>
</ng-container>
<!-- MAX COST -->
<ng-container>
<tr class="group-first">
<td class="item">
<b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: var(--primary)" class="p-1 pl-0">
{{ maxCost | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
<tr class="info group-last">
<td class="info" colspan=3>
<i><small><ng-container *ngTemplateOutlet="acceleratedTo; context: {$implicit: (estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize }"></ng-container></small></i>
</td>
</tr>
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item" i18n="accelerator.available-balance">Available balance</td>
<td class="amt">
{{ estimate.userBalance | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
<!-- LOGIN CTA -->
<ng-container *ngIf="stateService.isMempoolSpaceBuild && !isLoggedIn()">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1" i18n="shared.sign-in">Sign In</a>
</td>
</tr>
</ng-container>
<ng-container *ngIf="!stateService.isMempoolSpaceBuild">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [href]="'https://mempool.space/tx/' + tx.txid + '#accelerate'" class="btn btn-purple flex-grow-1" i18n="accelerator.accelerate-on-mempoolspace">Accelerate on mempool.space</a>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>
<div class="row mb-3" *ngIf="isLoggedIn()">
<div class="col">
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()" i18n="transaction.accelerate|Accelerate button label">Accelerate</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>
<ng-template #loadingEstimate>
<div class="skeleton-loader"></div>
<br>
</ng-template>
<ng-template #acceleratedTo let-i i18n="accelerator.accelerated-to-description">If your tx is accelerated to ~{{ i | number : '1.0-0' }} sat/vB</ng-template>

View File

@@ -1,116 +0,0 @@
.fee-card {
padding: 15px;
background-color: var(--bg);
.feerate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.rate {
font-size: 0.9em;
.symbol {
color: white;
}
}
}
}
.btn-border {
border: solid 1px black;
background-color: #0c4a87;
}
.feerate.active {
background-color: var(--primary) !important;
opacity: 1;
border: 1px solid #007fff !important;
}
.feerate:focus {
box-shadow: none !important;
}
.estimateDisabled {
opacity: 0.5;
pointer-events: none;
}
.table-toggle {
width: 100%;
margin-top: 0.5em;
}
.tab {
&:first-child {
margin-right: 1px;
}
border: solid 1px black;
border-bottom: none;
background-color: #323655;
border-top-left-radius: 10px !important;
border-top-right-radius: 10px !important;
}
.tab.active {
background-color: #5d659d !important;
opacity: 1;
}
.tab:focus {
box-shadow: none !important;
}
.table-accelerator {
tr {
td {
padding-top: 0;
padding-bottom: 0;
vertical-align: baseline;
}
&.group-first {
td {
padding-top: 0.75rem;
}
}
&.group-last {
td {
padding-bottom: 0.75rem;
}
}
}
td {
&:first-child {
width: 100vw;
}
&.info {
color: #6c757d;
white-space: initial;
}
&.amt {
text-align: right;
padding-right: 0.2em;
}
&.units {
padding-left: 0.2em;
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
.accelerate-cols {
display: flex;
flex-direction: row;
align-items: stretch;
margin-top: 1em;
}
.item {
white-space: initial;
}
.table-background {
background-color: var(--bg);
}

View File

@@ -1,240 +0,0 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core';
import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { nextRoundNumber } from '../../shared/common.utils';
import { ServicesApiServices } from '../../services/services-api.service';
import { AudioService } from '../../services/audio.service';
import { StateService } from '../../services/state.service';
export type AccelerationEstimate = {
txSummary: TxSummary;
nextBlockFee: number;
targetFeeRate: number;
userBalance: number;
enoughBalance: boolean;
cost: number;
mempoolBaseFee: number;
vsizeFee: number;
}
export type TxSummary = {
txid: string; // txid of the current transaction
effectiveVsize: number; // Total vsize of the dependency tree
effectiveFee: number; // Total fee of the dependency tree in sats
ancestorCount: number; // Number of ancestors
}
export interface RateOption {
fee: number;
rate: number;
index: number;
}
export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
@Component({
selector: 'app-accelerate-preview',
templateUrl: 'accelerate-preview.component.html',
styleUrls: ['accelerate-preview.component.scss']
})
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
@Input() tx: Transaction | undefined;
@Input() scrollEvent: boolean;
math = Math;
error = '';
showSuccess = false;
estimateSubscription: Subscription;
accelerationSubscription: Subscription;
estimate: any;
hasAncestors: boolean = false;
minExtraCost = 0;
minBidAllowed = 0;
maxBidAllowed = 0;
defaultBid = 0;
maxCost = 0;
userBid = 0;
accelerationUUID: string;
selectFeeRateIndex = 1;
isMobile: boolean = window.innerWidth <= 767.98;
user: any = undefined;
maxRateOptions: RateOption[] = [];
constructor(
public stateService: StateService,
private servicesApiService: ServicesApiServices,
private storageService: StorageService,
private audioService: AudioService,
private cd: ChangeDetectorRef
) {
}
ngOnDestroy(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
}
ngOnInit() {
this.accelerationUUID = window.crypto.randomUUID();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
}
ngAfterViewInit() {
this.user = this.storageService.getAuth()?.user ?? null;
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
this.estimate = undefined;
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
} else {
this.estimate = response.body;
if (!this.estimate) {
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
}
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.error = `not_enough_balance`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
}
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
// Make min extra fee at least 50% of the current tx fee
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
return {
fee: this.minExtraCost * multiplier,
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
index,
};
});
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
this.userBid = this.defaultBid;
if (this.userBid < this.minBidAllowed) {
this.userBid = this.minBidAllowed;
} else if (this.userBid > this.maxBidAllowed) {
this.userBid = this.maxBidAllowed;
}
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
setTimeout(() => {
this.onScroll();
}, 100);
}
}
}),
catchError((response) => {
this.estimate = undefined;
this.error = response.error;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
return of(null);
})
).subscribe();
}
/**
* User changed his bid
*/
setUserBid({ fee, index }: { fee: number, index: number}) {
if (this.estimate) {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
}
}
/**
* Scroll to element id with or without setTimeout
*/
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
setTimeout(() => {
this.scrollToPreview(id, position);
}, 100);
}
scrollToPreview(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
this.cd.markForCheck();
acceleratePreviewAnchor.scrollIntoView({
behavior: 'smooth',
inline: position,
block: position,
});
}
}
/**
* Send acceleration request
*/
accelerate() {
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid,
this.userBid,
this.accelerationUUID
).subscribe({
next: () => {
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.scrollToPreviewWithTimeout('successAlert', 'center');
this.estimateSubscription.unsubscribe();
},
error: (response) => {
if (response.status === 403 && response.error === 'not_available') {
this.error = 'waitlisted';
} else {
this.error = response.error;
}
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
});
}
isLoggedIn() {
const auth = this.storageService.getAuth();
return auth !== null;
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
@HostListener('window:scroll', ['$event']) // for window scroll events
onScroll() {
if (this.estimate) {
setTimeout(() => {
this.onScroll();
}, 200);
return;
}
}
}

View File

@@ -0,0 +1,133 @@
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
<div class="timeline-wrapper">
@if (!tx.status.confirmed) {
<div class="timeline">
<div class="intervals">
<div class="node-spacer"></div>
<div class="interval-spacer"></div>
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
@if (eta) {
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
}
</div>
</div>
<div class="node-spacer"></div>
</div>
<div class="nodes">
<div class="node-spacer"></div>
<div class="interval-spacer"></div>
<div class="node">
<div class="acc-to-confirmed right go-faster"></div>
</div>
<div class="interval-spacer">
</div>
<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>
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
</div>
</div>
</div>
}
<div class="timeline">
<div class="intervals">
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
<app-time [time]="acceleratedAt - transactionTime"></app-time>
</div>
</div>
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
@if (tx.status.confirmed) {
<div class="interval-time">
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
</div>
} @else if (standardETA && !tx.status.confirmed) {
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
}
</div>
</div>
<div class="node-spacer"></div>
</div>
<div class="nodes">
<div class="node" [id]="'first-seen'">
<div class="seen-to-acc right"></div>
<div class="shape-border">
<div class="shape"></div>
</div>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
<div class="time">
@if (useAbsoluteTime) {
<span>{{ transactionTime * 1000 | date }}</span>
} @else {
<app-time kind="since" [time]="transactionTime"></app-time>
}
</div>
</div>
<div class="interval-spacer">
<div class="seen-to-acc"></div>
</div>
<div class="node" [class.accelerated]="!tx.status.confirmed" [id]="'accelerated'">
<div class="seen-to-acc left"></div>
@if (tx.status.confirmed) {
<div class="acc-to-confirmed right"></div>
} @else {
<div class="seen-to-acc right"></div>
}
<div class="shape-border">
<div class="shape"></div>
@if (!tx.status.confirmed) {
<div class="connector down loading"></div>
}
</div>
@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">
@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>
}
</div>
</div>
<div class="interval-spacer">
@if (tx.status.confirmed) {
<div class="acc-to-confirmed"></div>
} @else {
<div class="seen-to-acc"></div>
}
</div>
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
@if (tx.status.confirmed) {
<div class="acc-to-confirmed left"></div>
} @else {
<div class="seen-to-acc left"></div>
}
<div class="shape-border" [class.waiting]="!tx.status.confirmed">
<div class="shape"></div>
</div>
@if (tx.status.confirmed) {
<div class="status"><span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span></div>
<div class="time">
@if (useAbsoluteTime) {
<span>{{ tx.status.block_time * 1000 | date }}</span>
} @else {
<app-time kind="since" [time]="tx.status.block_time"></app-time>
}
</div>
}
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,265 @@
.acceleration-timeline {
position: relative;
width: 100%;
padding: 1em 0;
&.lower-padding {
padding: 0.5em 0 1em;
}
&::after, &::before {
content: '';
display: block;
position: absolute;
top: 0;
bottom: 0;
width: 2em;
z-index: 2;
}
&::before {
left: 0;
background: linear-gradient(to right, var(--box-bg), var(--box-bg), transparent);
}
&::after {
right: 0;
background: linear-gradient(to left, var(--box-bg), var(--box-bg), transparent);
}
.timeline-wrapper {
position: relative;
width: calc(100% - 2em);
margin: auto;
overflow-x: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.intervals, .nodes {
min-width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
text-align: center;
.node, .node-spacer {
width: 6em;
min-width: 6em;
flex-grow: 1;
}
.interval, .interval-spacer {
width: 8em;
min-width: 8em;
max-width: 8em;
height: 32px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-end;
}
.interval {
overflow: visible;
}
.interval-time {
font-size: 12px;
line-height: 16px;
white-space: nowrap;
.compare {
font-style: italic;
color: var(--mainnet-alt);
font-weight: 600;
@media (max-width: 600px) {
display: none;
}
}
}
}
.node, .interval-spacer {
position: relative;
.seen-to-acc {
position: absolute;
height: 10px;
left: -5px;
right: -5px;
top: 0;
transform: translateY(-50%);
background: var(--primary);
border-radius: 5px;
&.left {
right: 50%;
}
&.right {
left: 50%;
}
}
.acc-to-confirmed {
position: absolute;
height: 10px;
left: -5px;
right: -5px;
top: 0;
transform: translateY(-50%);
background: var(--tertiary);
border-radius: 5px;
&.go-faster {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath style='fill:%239339f4;' d='M 0,0 5,5 0,10 Z'/%3E%3Cpath style='fill:%23653b9c;' d='M 0,0 10,0 15,5 10,10 0,10 5,5 Z'/%3E%3Cpath style='fill:%239339f4;' d='M 10,0 20,0 20,10 10,10 15,5 Z'/%3E%3C/svg%3E%0A"); background-size: 20px 10px;
border-radius: 0;
&.right {
left: calc(50% + 5px);
margin-right: calc(-4em + 5px);
animation: goFasterRight 0.8s infinite linear;
}
&.left {
right: calc(50% + 5px);
margin-left: calc(-4em + 5px);
animation: goFasterLeft 0.8s infinite linear;
}
}
&.left {
right: 50%;
}
&.right {
left: 50%;
}
}
}
.nodes {
position: relative;
margin-top: 1em;
.node {
.shape-border {
display: block;
margin: auto;
height: calc(1em + 8px);
width: calc(1em + 8px);
margin-bottom: -8px;
transform: translateY(-50%);
border-radius: 50%;
cursor: pointer;
padding: 4px;
background: transparent;
.shape {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
background: white;
z-index: 1;
}
&.waiting {
.shape {
background: var(--grey);
}
}
.connector {
position: absolute;
z-index: 0;
height: 88px;
width: 10px;
left: -5px;
top: -73px;
transform: translateX(120%);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='20'%3E%3Cpath style='fill:%239339f4;' d='M 0,20 5,15 10,20 Z'/%3E%3Cpath style='fill:%23653b9c;' d='M 0,20 5,15 10,20 10,10 5,5 0,10 Z'/%3E%3Cpath style='fill:%239339f4;' d='M 0,10 5,5 10,10 10,0 0,0 Z'/%3E%3C/svg%3E%0A"); // linear-gradient(135deg, var(--tertiary) 34%, transparent 34%),
background-size: 10px 20px;
&.down {
border-top-left-radius: 10px;
}
&.up {
border-top-right-radius: 10px;
}
&.loading {
animation: goFasterUp 0.8s infinite linear;
}
}
}
&.accelerated {
.shape-border {
animation: acceleratePulse 0.4s infinite;
}
}
&.selected {
.shape-border {
background: var(--mainnet-alt);
}
}
.status {
margin-top: -66px;
.badge.badge-waiting {
opacity: 0.5;
background-color: var(--grey);
color: white;
}
.badge.badge-accelerated {
background-color: var(--tertiary);
color: white;
}
}
.time {
margin-top: 32px;
font-size: 12px;
line-height: 16px;
white-space: nowrap;
&.offset-left {
@media (max-width: 650px) {
margin-left: -20px;
}
}
&.no-margin {
margin-top: 0px;
}
}
}
}
}
@keyframes acceleratePulse {
0% { background-color: var(--tertiary) }
50% { background-color: var(--mainnet-alt) }
100% { background-color: var(--tertiary) }
}
@keyframes goFasterUp {
0% { background-position-y: 0; }
100% { background-position-y: -40px; }
}
@keyframes goFasterLeft {
0% { background-position: left 0px bottom 0px }
100% { background-position: left 40px bottom 0px; }
}
@keyframes goFasterRight {
0% { background-position: right 0 bottom 0px; }
100% { background-position: right -40px bottom 0px; }
}

View File

@@ -0,0 +1,55 @@
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
@Component({
selector: 'app-acceleration-timeline',
templateUrl: './acceleration-timeline.component.html',
styleUrls: ['./acceleration-timeline.component.scss'],
})
export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;
@Input() tx: Transaction;
@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)
@Input() standardETA: number;
@Input() acceleratedETA: number;
acceleratedAt: number;
now: number;
accelerateRatio: number;
useAbsoluteTime: boolean = false;
interval: number;
constructor() {}
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.interval = window.setInterval(() => {
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
}, 60000);
}
ngOnChanges(changes): void {
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
// if (changes?.eta?.currentValue) {
// if (changes?.acceleratedETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
// } else if (changes?.standardETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
// }
// }
// }
}
ngOnDestroy(): void {
clearInterval(this.interval);
}
}

View File

@@ -45,8 +45,8 @@
</form>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" [style]="{opacity: isLoading ? 0.5 : 1}">
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, opacity: isLoading ? 0.5 : 1 }" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { EChartsOption } from '../../../graphs/echarts';
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
import { startWith, switchMap, tap } from 'rxjs/operators';
@@ -8,10 +8,11 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
import { StorageService } from '../../../services/storage.service';
import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { StateService } from '../../../services/state.service';
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-acceleration-fees-graph',
@@ -32,7 +33,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
@Input() height: number = 300;
@Input() right: number | string = 45;
@Input() left: number | string = 75;
@Input() period: '3d' | '1w' | '1m' = '1w';
@Input() period: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
@Input() accelerations$: Observable<Acceleration[]>;
miningWindowPreference: string;
@@ -48,7 +49,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
isLoading = true;
formatNumber = formatNumber;
timespan = '';
periodSubject$: Subject<'3d' | '1w' | '1m'> = new Subject();
periodSubject$: Subject<'24h' | '3d' | '1w' | '1m' | 'all'> = new Subject();
chartInstance: any = undefined;
daysAvailable: number = 0;
@@ -62,6 +63,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
private route: ActivatedRoute,
public stateService: StateService,
private cd: ChangeDetectorRef,
private router: Router,
private zone: NgZone,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1w' });
this.radioGroupForm.controls.dateSpan.setValue('1w');
@@ -78,7 +81,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
this.route.fragment.subscribe((fragment) => {
if (['24h', '3d', '1w', '1m', '3m'].indexOf(fragment) > -1) {
if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
});
@@ -294,6 +297,19 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
onChartInit(ec) {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
this.zone.run(() => {
if (['24h', '3d'].includes(this.timespan)) {
const url = new RelativeUrlPipe(this.stateService).transform(`/block/${e.data[2]}`);
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
window.open(url);
} else {
this.router.navigate([url]);
}
}
});
});
}
isMobile() {

View File

@@ -4,7 +4,7 @@
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
<div class="card-text">
<div>{{ stats.totalRequested }}</div>
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
<div class="symbol" i18n="accelerator.total-accelerated-plural">accelerated</div>
</div>
</div>
<div class="item">

View File

@@ -16,7 +16,7 @@ export type AccelerationStats = {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationStatsComponent implements OnInit, OnChanges {
@Input() timespan: '3d' | '1w' | '1m' = '1w';
@Input() timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
accelerationStats$: Observable<AccelerationStats>;
blocksInPeriod: number = 7 * 144;
@@ -35,6 +35,9 @@ export class AccelerationStatsComponent implements OnInit, OnChanges {
updateStats(): void {
this.accelerationStats$ = this.servicesApiService.getAccelerationStats$({ timeframe: this.timespan });
switch (this.timespan) {
case '24h':
this.blocksInPeriod = 144;
break;
case '3d':
this.blocksInPeriod = 3 * 144;
break;
@@ -44,6 +47,9 @@ export class AccelerationStatsComponent implements OnInit, OnChanges {
case '1m':
this.blocksInPeriod = 30 * 144;
break;
case 'all':
this.blocksInPeriod = Infinity;
break;
}
}
}

View File

@@ -35,7 +35,7 @@
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td>
<td class="time text-right">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time>
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
</td>
</ng-container>
<ng-container *ngIf="!pending">
@@ -55,7 +55,7 @@
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger" i18n="accelerator.canceled">Failed <span *ngIf="acceleration.status === 'failed_provisional'">🔄</span></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time>
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
</td>
</ng-container>
</tr>

View File

@@ -1,9 +1,11 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
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 { 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';
@Component({
selector: 'app-accelerations-list',
@@ -25,25 +27,66 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = [];
pageSubject: BehaviorSubject<number> = new BehaviorSubject(this.page);
keyNavigationSubscription: Subscription;
dir: 'rtl' | 'ltr' = 'ltr';
paramSubscription: Subscription;
constructor(
private servicesApiService: ServicesApiServices,
private websocketService: WebsocketService,
public stateService: StateService,
private cd: ChangeDetectorRef,
private seoService: SeoService,
private route: ActivatedRoute,
private router: Router,
@Inject(LOCALE_ID) private locale: string,
) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
this.dir = 'rtl';
}
}
ngOnInit(): void {
if (!this.widget) {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
this.paramSubscription = this.route.params.pipe(
tap(params => {
this.page = +params['page'] || 1;
this.pageSubject.next(this.page);
})
).subscribe();
const prevKey = this.dir === 'ltr' ? 'ArrowLeft' : 'ArrowRight';
const nextKey = this.dir === 'ltr' ? 'ArrowRight' : 'ArrowLeft';
this.keyNavigationSubscription = this.stateService.keyNavigation$.pipe(
filter((event) => event.key === prevKey || event.key === nextKey),
tap((event) => {
if (event.key === prevKey && this.page > 1) {
this.page--;
this.isLoading = true;
this.cd.markForCheck();
}
if (event.key === nextKey && this.page * 15 < this.accelerationCount) {
this.page++;
this.isLoading = true;
this.cd.markForCheck();
}
}),
throttleTime(1000, undefined, { leading: true, trailing: true }),
).subscribe(() => {
this.pageChange(this.page);
});
}
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.accelerationList$ = this.pageSubject.pipe(
switchMap((page) => {
this.isLoading = true;
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.stateService.liveAccelerations$ : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page }));
if (!this.accelerations$ && this.pending) {
this.websocketService.ensureTrackAccelerations();
@@ -82,7 +125,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
}
pageChange(page: number): void {
this.pageSubject.next(page);
this.router.navigate(['acceleration', 'list', page]);
}
trackByBlock(index: number, block: BlockExtended): number {
@@ -91,5 +134,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.websocketService.stopTrackAccelerations();
this.paramSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe();
}
}

View File

@@ -23,12 +23,18 @@
<div class="main-title">
<span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>&nbsp;
@switch (timespan) {
@case ('24h') {
<span style="font-size: xx-small" i18n="mining.1-day">(1 day)</span>
}
@case ('1w') {
<span style="font-size: xx-small" i18n="mining.1-week">(1 week)</span>
}
@case ('1m') {
<span style="font-size: xx-small" i18n="mining.1-month">(1 month)</span>
}
@case ('all') {
<span style="font-size: xx-small" i18n="mining.all-time">(all time)</span>
}
}
</div>
<div class="card-wrapper">
@@ -36,11 +42,17 @@
<div class="card-body more-padding">
<app-acceleration-stats [timespan]="timespan"></app-acceleration-stats>
<div class="widget-toggler">
<a href="" (click)="setTimespan('24h')" class="toggler-option"
[ngClass]="{'inactive': timespan === '24h'}"><small>24h</small></a>
<span style="color: #ffffff66; font-size: 8px"> | </span>
<a href="" (click)="setTimespan('1w')" class="toggler-option"
[ngClass]="{'inactive': timespan === '1w'}"><small>1w</small></a>
<span style="color: #ffffff66; font-size: 8px"> | </span>
<a href="" (click)="setTimespan('1m')" class="toggler-option"
[ngClass]="{'inactive': timespan === '1m'}"><small>1m</small></a>
<span style="color: #ffffff66; font-size: 8px"> | </span>
<a href="" (click)="setTimespan('all')" class="toggler-option"
[ngClass]="{'inactive': timespan === 'all'}"><small>all</small></a>
</div>
</div>
</div>

View File

@@ -37,7 +37,7 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
webGlEnabled = true;
seen: Set<string> = new Set();
firstLoad = true;
timespan: '3d' | '1w' | '1m' = '1w';
timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
accelerationDeltaSubscription: Subscription;
@@ -96,10 +96,16 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
share(),
);
this.minedAccelerations$ = this.accelerations$.pipe(
map(accelerations => {
return accelerations.filter(acc => ['completed_provisional', 'completed'].includes(acc.status));
})
this.minedAccelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
switchMap(() => {
return this.serviceApiServices.getAccelerationHistory$({ status: 'completed_provisional,completed', pageLength: 6 }).pipe(
catchError(() => {
return of([]);
}),
);
}),
share(),
);
this.blocks$ = combineLatest([

View File

@@ -0,0 +1,66 @@
@if (chartOnly) {
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
} @else {
<table>
<tbody>
<tr>
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
<td class="pie-chart" rowspan="2" *ngIf="chartPositionLeft">
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
</td>
<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>
} @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
</div>
</td>
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<div class="d-flex justify-content-between align-items-start">
@if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
}
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
</div>
</td>
</tr>
<tr>
<td class="td-width field-label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td class="field-value" *ngIf="acceleratedByPercentage">
<ng-container i18n="accelerator.x-of-hash-rate">{{ acceleratedByPercentage }} <span class="symbol hashrate-label">of hashrate</span></ng-container>
</td>
</tr>
@if (hasCpfp && chartPositionLeft) {
<tr>
<td colspan="3" class="pt-0">
<div class="d-flex justify-content-end align-items-start">
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
</div>
</td>
</tr>
}
</tbody>
</table>
}
<ng-template #pieChart>
<div class="chart-container">
@if (chartOptions && miningStats) {
<div
echarts
*browserOnly
class="chart"
[initOpts]="chartInitOptions"
[options]="chartOptions"
style="height: 72px; width: 72px;"
(chartInit)="onChartInit($event)"
></div>
} @else {
<div class="chart-loading">
<div class="spinner-border text-light"></div>
</div>
}
</div>
</ng-template>

View File

@@ -0,0 +1,64 @@
.td-width {
width: 150px;
min-width: 150px;
@media (max-width: 768px) {
width: 175px;
min-width: 175px;
}
}
.field-label {
@media (max-width: 849px) {
text-align: left;
}
@media (max-width: 649px) {
width: auto;
min-width: auto;
}
&.chart-left {
width: 100%;
}
}
.field-value {
@media (max-width: 849px) {
width: 100%;
}
&.chart-left {
width: auto;
}
.hashrate-label {
@media (max-width: 420px) {
display: none;
}
}
}
.pie-chart {
width: 100%;
vertical-align: middle;
text-align: center;
.chart-container {
width: 72px;
height: 100%;
margin-left: auto;
}
@media (max-width: 850px) {
width: 150px;
}
@media (max-width: 420px) {
padding-left: 0;
}
}
::ng-deep .chart {
overflow: visible;
& > div, & > div > svg {
overflow: visible !important;
}
}

View File

@@ -0,0 +1,142 @@
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core';
import { Transaction } from '../../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
import { MiningStats } from '../../../services/mining.service';
function lighten(color, p): { r, g, b } {
return {
r: color.r + ((255 - color.r) * p),
g: color.g + ((255 - color.g) * p),
b: color.b + ((255 - color.b) * p),
};
}
function toRGB({r,g,b}): string {
return `rgb(${r},${g},${b})`;
}
@Component({
selector: 'app-active-acceleration-box',
templateUrl: './active-acceleration-box.component.html',
styleUrls: ['./active-acceleration-box.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActiveAccelerationBox implements OnChanges {
@Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() miningStats: MiningStats;
@Input() pools: number[];
@Input() hasCpfp: boolean = false;
@Input() chartOnly: boolean = false;
@Input() chartPositionLeft: boolean = false;
@Output() toggleCpfp = new EventEmitter();
acceleratedByPercentage: string = '';
chartOptions: EChartsOption;
chartInitOptions = {
renderer: 'svg',
};
timespan = '';
chartInstance: any = undefined;
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
if (pools && this.miningStats) {
this.prepareChartOptions(pools);
}
}
getChartData(poolList: number[]) {
const data: object[] = [];
const pools: { [id: number]: SinglePoolStats } = {};
for (const pool of this.miningStats.pools) {
pools[pool.poolUniqueId] = pool;
}
const getDataItem = (value, color, tooltip, emphasis) => ({
value,
name: tooltip,
itemStyle: {
color,
},
});
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);
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)),
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true,
) as PieSeriesOption);
})
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
data.push(getDataItem(
(this.miningStats.lastEstimatedHashrate - totalAcceleratedHashrate),
'rgba(127, 127, 127, 0.3)',
$localize`not accelerating` + ` (${notAcceleratedByPercentage})`,
false,
) as PieSeriesOption);
return data;
}
prepareChartOptions(pools: number[]) {
this.chartOptions = {
animation: false,
grid: {
top: 0,
right: 0,
bottom: 0,
left: 0,
},
tooltip: {
show: true,
trigger: 'item',
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: 'var(--tooltip-grey)',
},
borderColor: '#000',
formatter: (item) => {
return item.name;
}
},
series: [
{
type: 'pie',
radius: '100%',
label: {
show: false
},
labelLine: {
show: false
},
animationDuration: 0,
data: this.getChartData(pools),
}
]
};
}
onChartInit(ec) {
if (this.chartInstance !== undefined) {
return;
}
this.chartInstance = ec;
}
onToggleCpfp(): void {
this.toggleCpfp.emit();
}
}

View File

@@ -2,7 +2,7 @@
<div [class.full-container]="!widget">
<ng-container *ngIf="!error">
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, paddingBottom: !widget && !allowZoom ? '10px' : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">

View File

@@ -46,7 +46,6 @@
display: flex;
flex: 1;
width: 100%;
padding-bottom: 10px;
padding-right: 10px;
}
.chart-widget {

View File

@@ -1,13 +1,16 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service';
import { PriceService } from '../../services/price.service';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
const periodSeconds = {
'1d': (60 * 60 * 24),
@@ -44,7 +47,13 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() widget: boolean = false;
data: any[] = [];
fiatData: any[] = [];
hoverData: any[] = [];
conversions: any;
allowZoom: boolean = false;
initialRight = this.right;
initialLeft = this.left;
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
subscription: Subscription;
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
@@ -66,6 +75,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
private amountShortenerPipe: AmountShortenerPipe,
private cd: ChangeDetectorRef,
private relativeUrlPipe: RelativeUrlPipe,
private priceService: PriceService,
private fiatCurrencyPipe: FiatCurrencyPipe,
private fiatShortenerPipe: FiatShortenerPipe,
private zone: NgZone,
) {}
ngOnChanges(changes: SimpleChanges): void {
@@ -86,10 +99,39 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
))
]).subscribe(([redraw, addressSummary]) => {
)),
this.stateService.conversions$
]).pipe(
switchMap(([redraw, addressSummary, conversions]) => {
this.conversions = conversions;
if (addressSummary) {
let extendedSummary = this.extendSummary(addressSummary);
return this.priceService.getPriceByBulk$(extendedSummary.map(d => d.time), 'USD').pipe(
tap((prices) => {
if (prices.length !== extendedSummary.length) {
extendedSummary = extendedSummary.map(item => ({ ...item, price: 0 }));
} else {
extendedSummary = extendedSummary.map((item, index) => {
let price = 0;
if (prices[index].price) {
price = prices[index].price['USD'];
} else if (this.conversions && this.conversions['USD']) {
price = this.conversions['USD'];
}
return { ...item, price: price }
});
}
}),
map(() => [redraw, extendedSummary, conversions])
)
} else {
return of([redraw, addressSummary, conversions]);
}
})
).subscribe(([redraw, addressSummary, conversions]) => {
if (addressSummary) {
this.error = null;
this.allowZoom = addressSummary.length > 100 && !this.widget;
this.prepareChartOptions(addressSummary);
}
this.isLoading = false;
@@ -101,25 +143,37 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
}
}
prepareChartOptions(summary): void {
prepareChartOptions(summary: AddressTxSummary[]) {
if (!summary || !this.stats) {
return;
}
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
this.data = summary.map(d => {
const balance = total;
total -= d.value;
return [d.time * 1000, balance, d];
const processData = summary.map(d => {
const balance = total;
const fiatBalance = total * d.price / 100_000_000;
total -= d.value;
return {
time: d.time * 1000,
balance,
fiatBalance,
d
};
}).reverse();
this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]);
this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]);
const now = Date.now();
if (this.period !== 'all') {
const now = Date.now();
const start = now - (periodSeconds[this.period] * 1000);
this.data = this.data.filter(d => d[0] >= start);
this.data.push(
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }}
);
const startFiat = this.data[0]?.[0] ?? start; // Make sure USD data starts at the same time as BTC data
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
}
this.data.push(
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }}
);
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
@@ -130,14 +184,42 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
{ offset: 0, color: '#FDD835' },
{ offset: 1, color: '#FB8C00' },
]),
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#4CAF50' },
{ offset: 1, color: '#1B5E20' },
]),
],
animation: false,
grid: {
top: 20,
bottom: 20,
bottom: this.allowZoom ? 65 : 20,
right: this.right,
left: this.left,
},
legend: !this.stateService.isAnyTestnet() ? {
data: [
{
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
inactiveColor: 'var(--grey)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
{
name: 'Fiat',
inactiveColor: 'var(--grey)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
}
],
selected: this.selected,
formatter: function (name) {
return name === 'Fiat' ? 'USD' : 'BTC';
}
} : undefined,
tooltip: {
show: !this.isMobile(),
trigger: 'axis',
@@ -152,27 +234,64 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
align: 'left',
},
borderColor: '#000',
formatter: function (data): string {
if (!data?.length || !data[0]?.data?.[2]?.txid) {
formatter: function (data) {
const btcData = data.filter(d => d.seriesName !== 'Fiat');
const fiatData = data.filter(d => d.seriesName === 'Fiat');
data = btcData.length ? btcData : fiatData;
if ((!btcData.length || !btcData[0]?.data?.[2]?.txid) && !fiatData.length) {
return '';
}
const header = data.length === 1
let tooltip = '<div>';
const hasTx = data[0].data[2].txid;
if (hasTx) {
const header = data.length === 1
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
: `${data.length} transactions`;
tooltip += `<span><b>${header}</b></span>`;
}
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
const val = data.reduce((total, d) => total + d.data[2].value, 0);
const color = val === 0 ? '' : (val > 0 ? 'var(--green)' : 'var(--red)');
const symbol = val > 0 ? '+' : '';
return `
<div>
<span><b>${header}</b></span>
<div style="text-align: right;">
<span style="color: ${color}">${symbol} ${(val / 100_000_000).toFixed(8)} BTC</span><br>
<span>${(data[0].data[1] / 100_000_000).toFixed(8)} BTC</span>
</div>
<span>${date}</span>
tooltip += `<div>
<div style="text-align: right;">`;
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
const fiatColor = fiatVal === 0 ? '' : (fiatVal > 0 ? 'var(--green)' : 'var(--red)');
const btcSymbol = btcVal > 0 ? '+' : '';
const fiatSymbol = fiatVal > 0 ? '+' : '';
if (btcData.length && fiatData.length) {
tooltip += `<div style="display: flex; justify-content: space-between; color: ${btcColor}">
<span style="text-align: left; margin-right: 10px;">${btcSymbol} ${formatBTC(btcVal, 4)} BTC</span>
<span style="text-align: right;">${fiatSymbol} ${formatFiat(fiatVal)}</span>
</div>
`;
<div style="display: flex; justify-content: space-between;">
<span style="text-align: left; margin-right: 10px;">${formatBTC(btcData[0].data[1], 4)} BTC</span>
<span style="text-align: right;">${formatFiat(fiatData[0].data[1])}</span>
</div>`;
} else if (btcData.length) {
tooltip += `<span style="color: ${btcColor}">${btcSymbol} ${formatBTC(btcVal, 8)} BTC</span><br>
<span>${formatBTC(data[0].data[1], 8)} BTC</span>`;
} else {
if (this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]) {
tooltip += `<div style="display: flex; justify-content: space-between;">
<span style="text-align: left; margin-right: 10px;">${formatBTC(data[0].data[3], 4)} BTC</span>
<span style="text-align: right;">${formatFiat(data[0].data[1])}</span>
</div>`;
} else {
tooltip += `${hasTx ? `<span style="color: ${fiatColor}">${fiatSymbol} ${formatFiat(fiatVal)}</span><br>` : ''}
<span>${formatFiat(data[0].data[1])}</span>`;
}
}
tooltip += `</div><span>${date}</span></div>`;
return tooltip;
}.bind(this)
},
xAxis: {
@@ -211,10 +330,24 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
},
min: this.period === 'all' ? 0 : 'dataMin'
},
{
type: 'value',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: function(val) {
return this.fiatShortenerPipe.transform(val, null, 'USD');
}.bind(this)
},
splitLine: {
show: false,
},
min: this.period === 'all' ? 0 : 'dataMin'
},
],
series: [
{
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
yAxisIndex: 0,
showSymbol: false,
symbol: 'circle',
symbolSize: 8,
@@ -226,14 +359,58 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'line',
smooth: false,
step: 'end'
}
}, !this.stateService.isAnyTestnet() ?
{
name: 'Fiat',
yAxisIndex: 1,
showSymbol: false,
symbol: 'circle',
symbolSize: 8,
data: this.fiatData,
areaStyle: {
opacity: 0.5,
},
triggerLineEvent: true,
type: 'line',
smooth: false,
step: 'end'
} : undefined
],
dataZoom: this.allowZoom ? [{
type: 'inside',
realtime: true,
zoomLock: true,
maxSpan: 100,
minSpan: 5,
moveOnMouseMove: false,
}, {
showDetail: false,
show: true,
type: 'slider',
brushSelect: false,
realtime: true,
left: this.left,
right: this.right,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
},
}] : undefined
};
}
onChartClick(e) {
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
this.router.navigate([this.relativeUrlPipe.transform('/tx/'), this.hoverData[0][2].txid]);
this.zone.run(() => {
const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`);
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
window.open(url);
} else {
this.router.navigate([url]);
}
});
}
}
@@ -241,10 +418,38 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
this.hoverData = (e?.dataByCoordSys?.[0]?.dataByAxis?.[0]?.seriesDataIndices || []).map(indices => this.data[indices.dataIndex]);
}
onLegendSelectChanged(e) {
this.selected = e.selected;
this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
this.chartOptions = {
grid: {
right: this.right,
left: this.left,
},
legend: {
selected: this.selected,
},
dataZoom: this.allowZoom ? [{
left: this.left,
right: this.right,
}, {
left: this.left,
right: this.right,
}] : undefined
};
if (this.chartInstance) {
this.chartInstance.setOption(this.chartOptions);
}
}
onChartInit(ec) {
this.chartInstance = ec;
this.chartInstance.on('showTip', this.onTooltip.bind(this));
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
this.chartInstance.on('legendselectchanged', this.onLegendSelectChanged.bind(this));
}
ngOnDestroy(): void {
@@ -256,4 +461,27 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
isMobile() {
return (window.innerWidth <= 767.98);
}
extendSummary(summary) {
let extendedSummary = summary.slice();
// Add a point at today's date to make the graph end at the current time
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
extendedSummary.reverse();
let oneHour = 60 * 60;
// Fill gaps longer than interval
for (let i = 0; i < extendedSummary.length - 1; i++) {
let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
if (hours > 1) {
for (let j = 1; j < hours; j++) {
let newTime = extendedSummary[i].time + oneHour * j;
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
}
i += hours - 1;
}
}
return extendedSummary.reverse();
}
}

View File

@@ -4,7 +4,7 @@
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">
<span
*ngIf="label"
class="badge badge-pill badge-warning"
class="badge badge-pill badge-warning {{ class }}"
>{{ label }}</span>
</a>
</div>
@@ -15,6 +15,6 @@
<ng-template #default>
<span
*ngIf="label"
class="badge badge-pill badge-warning"
class="badge badge-pill badge-warning {{ class }}"
>{{ label }}</span>
</ng-template>

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { Vin, Vout } from '../../interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
import { parseMultisigScript } from '../../bitcoin.utils';
import { AddressType, AddressTypeInfo } from '../../shared/address-utils';
@Component({
selector: 'app-address-labels',
@@ -12,9 +12,11 @@ import { parseMultisigScript } from '../../bitcoin.utils';
export class AddressLabelsComponent implements OnChanges {
network = '';
@Input() address: AddressTypeInfo;
@Input() vin: Vin;
@Input() vout: Vout;
@Input() channel: any;
@Input() class: string = '';
label?: string;
@@ -27,6 +29,8 @@ export class AddressLabelsComponent implements OnChanges {
ngOnChanges() {
if (this.channel) {
this.handleChannel();
} else if (this.address) {
this.handleAddress();
} else if (this.vin) {
this.handleVin();
} else if (this.vout) {
@@ -41,74 +45,32 @@ export class AddressLabelsComponent implements OnChanges {
this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`;
}
handleVin() {
if (this.vin.inner_witnessscript_asm) {
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || this.vin.inner_witnessscript_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) {
if (this.vin.witness.length > 11) {
this.label = 'Liquid Peg Out';
} else {
this.label = 'Emergency Liquid Peg Out';
}
return;
handleAddress() {
if (this.address?.scripts.size) {
const script = this.address?.scripts.values().next().value;
if (script.template?.label) {
this.label = script.template.label;
}
const topElement = this.vin.witness[this.vin.witness.length - 2];
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) {
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
if (topElement === '01') {
// top element is '01' to get in the revocation path
this.label = 'Revoked Lightning Force Close';
} else {
// top element is '', this is a delayed to_local output
this.label = 'Lightning Force Close';
}
return;
} else if (
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm) ||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)
) {
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
if (topElement.length === 66) {
// top element is a public key
this.label = 'Revoked Lightning HTLC';
} else if (topElement) {
// top element is a preimage
this.label = 'Lightning HTLC';
} else {
// top element is '' to get in the expiry of the script
this.label = 'Expired Lightning HTLC';
}
return;
} else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
// https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
if (topElement) {
// top element is a signature
this.label = 'Lightning Anchor';
} else {
// top element is '', it has been swept after 16 blocks
this.label = 'Swept Lightning Anchor';
}
return;
}
this.detectMultisig(this.vin.inner_witnessscript_asm);
}
this.detectMultisig(this.vin.inner_redeemscript_asm);
this.detectMultisig(this.vin.prevout.scriptpubkey_asm);
}
detectMultisig(script: string) {
const ms = parseMultisigScript(script);
if (ms) {
this.label = $localize`:@@address-label.multisig:Multisig ${ms.m}:multisigM: of ${ms.n}:multisigN:`;
handleVin() {
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
if (address?.scripts.size) {
const script = address?.scripts.values().next().value;
if (script.template?.label) {
this.label = script.template.label;
}
}
}
handleVout() {
this.detectMultisig(this.vout.scriptpubkey_asm);
const address = new AddressTypeInfo(this.network || 'mainnet', this.vout.scriptpubkey_address, this.vout.scriptpubkey_type as AddressType, undefined, this.vout);
if (address?.scripts.size) {
const script = address?.scripts.values().next().value;
if (script.template?.label) {
this.label = script.template.label;
}
}
}
}

View File

@@ -14,7 +14,7 @@
</td>
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td>
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true"></app-time></td>
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
</tr>
</tbody>
<div class="">&nbsp;</div>

View File

@@ -58,7 +58,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
return summary?.slice(0, 6);
}),
switchMap(txs => {
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, true, this.currency).pipe(
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
map(price => {
return {
...tx,

View File

@@ -3,7 +3,13 @@
<h1 i18n="shared.address">Address</h1>
<div class="tx-link">
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
<app-clipboard [text]="addressString"></app-clipboard>
<span class="qrSpan" (mouseover)="showQR = true" (mouseout)="showQR = false" (pointerdown)="showQR = true">
<fa-icon [icon]="['fas', 'qrcode']" [fixedWidth]="true" [style.font-size]="isMobile ? '18px' : '12px'"></fa-icon>
<div class="qr-wrapper" [hidden]="!showQR">
<app-qrcode [size]="200" [data]="addressString"></app-qrcode>
</div>
</span>
<app-clipboard [text]="addressString" [size]="isMobile ? 'large' : 'normal'"></app-clipboard>
</app-truncate>
</div>
</div>
@@ -14,40 +20,56 @@
<div class="box">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped address-table">
<tbody>
<tr *ngIf="addressInfo && addressInfo.unconfidential">
<td i18n="address.unconfidential">Unconfidential</td>
<td>
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="8" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]">
<app-clipboard [text]="addressInfo.unconfidential"></app-clipboard>
</app-truncate>
</td>
</tr>
<ng-template [ngIf]="!address.electrum">
<tr>
<td i18n="address.total-received">Total received</td>
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="received" [noFiat]="true"></app-amount></td>
</tr>
<tr>
<td i18n="address.total-sent">Total sent</td>
<td *ngIf="address.chain_stats.spent_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="sent" [noFiat]="true"></app-amount></td>
</tr>
</ng-template>
<tr>
<td i18n="address.balance">Balance</td>
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="received - sent" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="received - sent"></app-fiat></span></td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col-md qrcode-col">
<div class="qr-wrapper">
<app-qrcode [data]="address.address"></app-qrcode>
@if (isMobile) {
<div class="col-sm">
<table class="table table-borderless table-striped address-table">
<tbody>
<tr><ng-container *ngTemplateOutlet="balanceRow"></ng-container></tr>
<tr><ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container></tr>
@if (!address.electrum) {
<tr><ng-container *ngTemplateOutlet="utxoRow"></ng-container></tr>
<tr><ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container></tr>
}
@if (network === 'liquid' || network === 'liquidtestnet') {
<tr><ng-container *ngTemplateOutlet="liquidRow"></ng-container></tr>
} @else if (!address.electrum) {
<tr><ng-container *ngTemplateOutlet="volumeRow"></ng-container></tr>
}
<tr><ng-container *ngTemplateOutlet="typeRow"></ng-container></tr>
</tbody>
</table>
</div>
</div>
} @else {
<div class="col-sm">
<table class="table table-borderless dual-col-striped table-fixed address-table">
<tbody>
<tr>
<ng-container *ngTemplateOutlet="balanceRow"></ng-container>
<ng-container *ngTemplateOutlet="spacerCell"></ng-container>
<ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container>
</tr>
@if (!address.electrum) {
<tr>
<ng-container *ngTemplateOutlet="utxoRow"></ng-container>
<ng-container *ngTemplateOutlet="spacerCell"></ng-container>
<ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container>
</tr>
}
<tr>
@if (network === 'liquid' || network === 'liquidtestnet') {
<ng-container *ngTemplateOutlet="liquidRow"></ng-container>
} @else if (!address.electrum) {
<ng-container *ngTemplateOutlet="volumeRow"></ng-container>
} @else {
<ng-container *ngTemplateOutlet="emptyTd"></ng-container>
}
<ng-container *ngTemplateOutlet="spacerCell"></ng-container>
<ng-container *ngTemplateOutlet="typeRow"></ng-container>
</tr>
</tbody>
</table>
</div>
}
</div>
</div>
@@ -66,7 +88,7 @@
</div>
<div class="row">
<div class="col-md">
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" [period]="balancePeriod" />
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" [period]="balancePeriod" left="80" />
</div>
</div>
</div>
@@ -76,8 +98,8 @@
<div class="title-tx">
<h2 class="text-left">
<ng-template [ngIf]="!transactions?.length">&nbsp;</ng-template>
<ng-template i18n="X of X Address Transaction" [ngIf]="transactions?.length === 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transaction</ng-template>
<ng-template i18n="X of X Address Transactions (Plural)" [ngIf]="transactions?.length > 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transactions</ng-template>
<ng-template i18n="X of X Address Transaction" [ngIf]="transactions?.length === 1">{{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transaction</ng-template>
<ng-template i18n="X of X Address Transactions (Plural)" [ngIf]="transactions?.length > 1">{{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transactions</ng-template>
</h2>
</div>
@@ -119,25 +141,54 @@
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col">
</div>
@if (isMobile) {
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
} @else {
<div class="col-sm">
<table class="table table-borderless dual-col-striped table-fixed">
<tbody>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
<ng-container *ngTemplateOutlet="spacerCell"></ng-container>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
<ng-container *ngTemplateOutlet="spacerCell"></ng-container>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
<ng-container *ngTemplateOutlet="spacerCell"></ng-container>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
}
</div>
</div>
@@ -182,3 +233,58 @@
<span class="skeleton-loader"></span>
</div>
</ng-template>
<ng-template #spacerCell>
<td class="spacer"></td>
</ng-template>
<ng-template #emptyTd>
<td class="spacer"></td>
<td class="spacer"></td>
</ng-template>
<ng-template #balanceRow>
<td i18n="address.confirmed-balance">Confirmed balance</td>
<td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd" class="wrap-cell"><app-amount [satoshis]="chainStats.balance" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="chainStats.balance"></app-fiat></span></td>
</ng-template>
<ng-template #pendingBalanceRow>
<td i18n="address.unconfirmed-balance" class="font-italic">Unconfirmed balance</td>
<td *ngIf="mempoolStats.funded_txo_sum !== undefined; else confidentialTd" class="font-italic wrap-cell"><app-amount [satoshis]="mempoolStats.balance" [noFiat]="true" [addPlus]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolStats.balance"></app-fiat></span></td>
</ng-template>
<ng-template #utxoRow>
<td i18n="address.confirmed-utxos">Confirmed UTXOs</td>
<td class="wrap-cell">{{ chainStats.utxos }}</td>
</ng-template>
<ng-template #pendingUtxoRow>
<td i18n="address.unconfirmed-utxos" class="font-italic">Unconfirmed UTXOs</td>
<td class="font-italic wrap-cell">{{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }}</td>
</ng-template>
<ng-template #volumeRow>
<td i18n="address.total-received">Total received</td>
<td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd" class="wrap-cell"><app-amount [satoshis]="chainStats.totalReceived"></app-amount></td>
</ng-template>
<ng-template #typeRow>
<td i18n="address.type">Type</td>
<td class="wrap-cell">
<span placement="bottom" class="badge badge-primary">
<app-address-type [address]="addressTypeInfo"></app-address-type>
</span>
<app-address-labels [channel]="exampleChannel" [address]="addressTypeInfo" class="ml-1"></app-address-labels>
</td>
</ng-template>
<ng-template #liquidRow>
<ng-container *ngIf="addressInfo && addressInfo.unconfidential">
<td i18n="address.unconfidential">Unconfidential</td>
<td>
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="8" [textAlign]="isMobile ? 'end' : 'start'" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]">
<app-clipboard [text]="addressInfo.unconfidential"></app-clipboard>
</app-truncate>
</td>
</ng-container>
</ng-template>

View File

@@ -1,16 +1,20 @@
.qr-wrapper {
position: absolute;
top: 30px;
right: 0px;
border: solid 10px var(--active-bg);
border-radius: 5px;
background-color: #fff;
padding: 10px;
padding-bottom: 5px;
display: inline-block;
display: block;
z-index: 99;
}
.qrcode-col {
margin: 20px auto 10px;
text-align: center;
@media (min-width: 992px){
margin: 0px auto 0px;
}
.qrSpan {
position: relative;
cursor: pointer;
padding-left: 0.4rem;
}
.fiat {
@@ -25,10 +29,14 @@
tr td {
&:last-child {
text-align: right;
@media (min-width: 576px) {
@media (min-width: 768px) {
text-align: left;
}
}
&.wrap-cell {
white-space: normal;
}
}
}
@@ -78,10 +86,10 @@ h1 {
top: 9px;
position: relative;
@media (min-width: 576px) {
max-width: calc(100% - 180px);
top: 11px;
}
@media (min-width: 768px) {
max-width: calc(100% - 180px);
top: 17px;
}
}
@@ -96,17 +104,6 @@ h1 {
.liquid-address {
.address-table {
table-layout: fixed;
tr td:first-child {
width: 170px;
}
tr td:last-child {
width: 80%;
}
}
.qrcode-col {
flex-grow: 0.5;
}
}

View File

@@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface';
import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
@@ -11,6 +11,83 @@ import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
import { AddressTypeInfo } from '../../shared/address-utils';
class AddressStats implements ChainStats {
address: string;
scriptpubkey?: string;
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
constructor (stats: ChainStats, address: string, scriptpubkey?: string) {
Object.assign(this, stats);
this.address = address;
this.scriptpubkey = scriptpubkey;
}
public addTx(tx: Transaction): void {
for (const vin of tx.vin) {
if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) {
this.spendTxo(vin.prevout.value);
}
}
for (const vout of tx.vout) {
if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) {
this.fundTxo(vout.value);
}
}
this.tx_count++;
}
public removeTx(tx: Transaction): void {
for (const vin of tx.vin) {
if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) {
this.unspendTxo(vin.prevout.value);
}
}
for (const vout of tx.vout) {
if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) {
this.unfundTxo(vout.value);
}
}
this.tx_count--;
}
private fundTxo(value: number): void {
this.funded_txo_sum += value;
this.funded_txo_count++;
}
private unfundTxo(value: number): void {
this.funded_txo_sum -= value;
this.funded_txo_count--;
}
private spendTxo(value: number): void {
this.spent_txo_sum += value;
this.spent_txo_count++;
}
private unspendTxo(value: number): void {
this.spent_txo_sum -= value;
this.spent_txo_count--;
}
get balance(): number {
return this.funded_txo_sum - this.spent_txo_sum;
}
get totalReceived(): number {
return this.funded_txo_sum;
}
get utxos(): number {
return this.funded_txo_count - this.spent_txo_count;
}
}
@Component({
selector: 'app-address',
@@ -20,6 +97,9 @@ import { AddressInformation } from '../../interfaces/node-api.interface';
export class AddressComponent implements OnInit, OnDestroy {
network = '';
isMobile: boolean;
showQR: boolean = false;
address: Address;
addressString: string;
isLoadingAddress = true;
@@ -33,11 +113,14 @@ export class AddressComponent implements OnInit, OnDestroy {
blockTxSubscription: Subscription;
addressLoadingStatus$: Observable<number>;
addressInfo: null | AddressInformation = null;
addressTypeInfo: null | AddressTypeInfo;
fullyLoaded = false;
txCount = 0;
received = 0;
sent = 0;
chainStats: AddressStats;
mempoolStats: AddressStats;
exampleChannel?: any;
now = Date.now() / 1000;
balancePeriod: 'all' | '1m' = 'all';
@@ -55,10 +138,12 @@ export class AddressComponent implements OnInit, OnDestroy {
private seoService: SeoService,
) { }
ngOnInit() {
ngOnInit(): void {
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.websocketService.want(['blocks']);
this.onResize();
this.addressLoadingStatus$ = this.route.paramMap
.pipe(
switchMap(() => this.stateService.loadingIndicators$),
@@ -75,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.isLoadingTransactions = true;
this.transactions = null;
this.addressInfo = null;
this.exampleChannel = null;
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
@@ -83,6 +169,8 @@ export class AddressComponent implements OnInit, OnDestroy {
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
this.addressTypeInfo = new AddressTypeInfo(this.stateService.network || 'mainnet', this.addressString);
return merge(
of(true),
this.stateService.connectionState$
@@ -175,13 +263,23 @@ export class AddressComponent implements OnInit, OnDestroy {
});
this.transactions = this.tempTransactions;
if (this.transactions.length === this.txCount) {
if (this.transactions.length === (this.mempoolStats.tx_count + this.chainStats.tx_count)) {
this.fullyLoaded = true;
}
this.isLoadingTransactions = false;
let addressVin: Vin[] = [];
for (const tx of this.transactions) {
addressVin = addressVin.concat(tx.vin.filter(v => v.prevout?.scriptpubkey_address === this.address.address));
}
this.addressTypeInfo.processInputs(addressVin);
// hack to trigger change detection
this.addressTypeInfo = this.addressTypeInfo.clone();
if (!this.showBalancePeriod()) {
this.setBalancePeriod('all');
} else {
this.setBalancePeriod('1m');
}
},
(error) => {
@@ -194,11 +292,13 @@ export class AddressComponent implements OnInit, OnDestroy {
this.mempoolTxSubscription = this.stateService.mempoolTransactions$
.subscribe(tx => {
this.addTransaction(tx);
this.mempoolStats.addTx(tx);
});
this.mempoolRemovedTxSubscription = this.stateService.mempoolRemovedTransactions$
.subscribe(tx => {
this.removeTransaction(tx);
this.mempoolStats.removeTx(tx);
});
this.blockTxSubscription = this.stateService.blockTransactions$
@@ -207,12 +307,14 @@ export class AddressComponent implements OnInit, OnDestroy {
if (tx) {
tx.status = transaction.status;
this.transactions = this.transactions.slice();
this.mempoolStats.removeTx(transaction);
this.audioService.playSound('magic');
} else {
if (this.addTransaction(transaction, false)) {
this.audioService.playSound('magic');
}
}
this.chainStats.addTx(transaction);
});
}
@@ -223,7 +325,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions.unshift(transaction);
this.transactions = this.transactions.slice();
this.txCount++;
if (playSound) {
if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
@@ -233,17 +334,6 @@ export class AddressComponent implements OnInit, OnDestroy {
}
}
transaction.vin.forEach((vin) => {
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
this.sent += vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout?.scriptpubkey_address === this.address.address) {
this.received += vout.value;
}
});
return true;
}
@@ -255,23 +345,11 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions.splice(index, 1);
this.transactions = this.transactions.slice();
this.txCount--;
transaction.vin.forEach((vin) => {
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
this.sent -= vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout?.scriptpubkey_address === this.address.address) {
this.received -= vout.value;
}
});
return true;
}
loadMore() {
loadMore(): void {
if (this.isLoadingTransactions || this.fullyLoaded) {
return;
}
@@ -299,10 +377,9 @@ export class AddressComponent implements OnInit, OnDestroy {
});
}
updateChainStats() {
this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum;
this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum;
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
updateChainStats(): void {
this.chainStats = new AddressStats(this.address.chain_stats, this.address.address);
this.mempoolStats = new AddressStats(this.address.mempool_stats, this.address.address);
}
setBalancePeriod(period: 'all' | '1m'): boolean {
@@ -317,7 +394,12 @@ export class AddressComponent implements OnInit, OnDestroy {
);
}
ngOnDestroy() {
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth < 768;
}
ngOnDestroy(): void {
this.mainSubscription.unsubscribe();
this.mempoolTxSubscription.unsubscribe();
this.mempoolRemovedTxSubscription.unsubscribe();

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="!noFiat && (viewAmountMode$ | async) === 'fiat' && (conversions$ | async) as conversions; else viewFiatVin">
<ng-container *ngIf="!noFiat && ignoreViewMode === false && (viewAmountMode$ | async) === 'fiat' && (conversions$ | async) as conversions; else viewFiatVin">
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
{{ addPlus && satoshis >= 0 ? '+' : '' }}{{
(
@@ -21,7 +21,7 @@
<span i18n="shared.confidential">Confidential</span>
</ng-template>
<ng-template #default>
@if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat') {
@if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat' || ignoreViewMode === true) {
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
<span class="symbol">
<ng-container *ngTemplateOutlet="prefix"></ng-container>BTC

View File

@@ -24,6 +24,7 @@ export class AmountComponent implements OnInit, OnDestroy {
@Input() addPlus = false;
@Input() blockConversion: Price;
@Input() forceBtc: boolean = false;
@Input() ignoreViewMode: boolean = false;
@Input() forceBlockConversion: boolean = false; // true = displays fiat price as 0 if blockConversion is undefined instead of falling back to conversions
constructor(

View File

@@ -0,0 +1,99 @@
<div class="wrapper">
@if (!minimal) {
<span *ngIf="paymentStatus === 3" class="valid-feedback d-block mt-5">
Payment successful. You can close this page.
</span>
<span *ngIf="paymentStatus === 4" class="valid-feedback d-block mt-5">
A transaction <a [href]="'/tx/' + invoice.cryptoInfo[0].payments[0].id.split('-')[0]">has been detected in the mempool</a> fully paying for this invoice. Waiting for on-chain confirmation.
</span>
}
<div *ngIf="paymentStatus === 2">
<form [formGroup]="paymentForm">
<div *ngIf="availableMethods.length > 1" class="form-group">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<!-- <label *ngIf="invoice.addresses.BTC" class="btn btn-primary" [ngClass]="{'active': paymentForm.get('method')?.value === 'chain'}">
<input type="radio" value="chain" formControlName="method"> <fa-icon [icon]="['fas', 'link']" [fixedWidth]="true" title="Onchain"></fa-icon>
</label> -->
<label *ngIf="invoice.addresses.BTC_LightningLike" class="btn btn-primary" [ngClass]="{'active': paymentForm.get('method')?.value === 'lightning'}">
<input type="radio" value="lightning" formControlName="method"> <fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" title="Lightning"></fa-icon>
</label>
<!-- <label *ngIf="invoice.addresses.LBTC" class="btn btn-primary" [ngClass]="{'active': paymentForm.get('method')?.value === 'lbtc'}">
<input type="radio" value="lbtc" formControlName="method"> <fa-icon [icon]="['fas', 'tint']" [fixedWidth]="true" title="Liquid Bitcoin"></fa-icon>
</label> -->
</div>
</div>
</form>
<ng-template [ngIf]="paymentForm.get('method')?.value === 'chain' && invoice">
<div class="qr-wrapper" [class.mt-0]="minimal">
<a [href]="bypassSecurityTrustUrl('bitcoin:' + invoice.addresses.BTC + '?amount=' + invoice.btcDue)" target="_blank">
<app-qrcode imageUrl="/resources/bitcoin-logo.png" [size]="200" [data]="'bitcoin:' + invoice.addresses.BTC + '?amount=' + invoice.btcDue"></app-qrcode>
</a>
</div>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control input-dark" readonly [value]="invoice.addresses.BTC">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="invoice.addresses.BTC"></app-clipboard></button>
</div>
</div>
@if (!minimal) {
<p>{{ invoice.btcDue | number: '1.0-8' }} <span class="symbol">BTC</span></p>
}
</ng-template>
<ng-template [ngIf]="paymentForm.get('method')?.value === 'lightning' && invoice && invoice.addresses.BTC_LightningLike">
<div class="qr-wrapper" [class.mt-0]="minimal">
<a [href]="bypassSecurityTrustUrl('lightning:' + invoice.addresses.BTC_LightningLike)" target="_blank">
<app-qrcode imageUrl="/resources/lightning-logo.png" [size]="200" [data]="invoice.addresses.BTC_LightningLike.toUpperCase()"></app-qrcode>
</a>
</div>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control input-dark" readonly [value]="invoice.addresses.BTC_LightningLike">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="invoice.addresses.BTC_LightningLike"></app-clipboard></button>
</div>
</div>
@if (!minimal) {
<p>{{ invoice.btcDue * 100_000_000 | number: '1.0-0' }} <span class="symbol">sats</span></p>
}
</ng-template>
<ng-template [ngIf]="invoice && (paymentForm.get('method')?.value === 'lbtc' || paymentForm.get('method')?.value === 'tlbtc')">
<div class="qr-wrapper" [class.mt-0]="minimal">
<a [href]="bypassSecurityTrustUrl('liquidnetwork:' + invoice.addresses.LBTC + '?amount=' + invoice.btcDue + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d')" target="_blank">
<app-qrcode imageUrl="/resources/liquid-bitcoin.png" [size]="200" [data]="'liquidnetwork:' + invoice.addresses.LBTC + '?amount=' + invoice.btcDue + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'"></app-qrcode>
</a>
</div>
<br>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control input-dark" readonly [value]="invoice.addresses.LBTC" />
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="invoice.addresses.LBTC"></app-clipboard></button>
</div>
</div>
@if (!minimal) {
<p>{{ invoice.btcDue | number: '1.0-8' }} <span class="symbol">BTC</span></p>
}
</ng-template>
@if (!minimal) {
<p>Waiting for transaction... </p>
<div class="spinner-border text-light"></div>
}
</div>
</div>

View File

@@ -0,0 +1,150 @@
.form-panel {
background-color: #292b45;
padding: 20px;
}
.sponsor-page {
text-align: center;
}
.qr-wrapper {
background-color: #FFF;
padding: 10px;
display: inline-block;
padding-bottom: 5px;
margin: 20px auto 0px;
}
.info-group {
max-width: 400px;
}
.card {
width: 240px;
height: 220px;
background-color: var(--bg);
border: 2px solid var(--bg);
cursor: pointer;
position: relative;
transition: 100ms all;
margin: 30px 30px 20px 30px;
@media(min-width: 476px) {
margin: 30px 100px 20px 100px;
}
@media(min-width: 851px) {
margin: 60px 20px 40px 20px;
}
.card-title {
font-weight: bold;
span {
font-weight: 100;
}
}
&.bigger {
height: 220px;
width: 240px;
margin-top: 40px;
}
&:hover {
background-color: #5058926b;
border: 2px solid #505892;
transform: scale(1.1) translateY(-10px);
margin-top: 70px;
.card-header {
background-color: #505892;
}
}
}
.donation-form {
max-width: 280px;
margin: auto;
button {
width: 100%;
}
}
.card-header {
background-color: #171929;
}
.flex-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.middle-card {
width: 280px;
height: 260px;
margin-top: 40px;
&:hover {
margin-top: 50px;
}
}
.shiny-border {
background-color: #5058926b;
border: 2px solid #505892;
transform: scale(1.1) translateY(-10px);
margin-top: 70px;
box-shadow: 0px 0px 100px #9858ff52;
.card-header {
background-color: #505892;
}
&.middle-card {
margin-top: 50px;
}
}
.input-group {
margin: 20px auto;
}
.donation-confirmed {
h2 {
margin-top: 50px;
span {
display: block;
&:last-child {
color: #9858ff;
font-weight: bold;
font-size: 2rem;
}
}
}
.order-details {
margin-top: 50px;
span {
color: #d81b60;
margin-left: 10px;
}
}
}
.card-body {
align-items: center;
display: flex;
justify-content: center;
flex-direction: column;
height: 100%;
}
.wrapper {
text-align: center;
width: 100%;
}
.input-dark {
background-color: var(--bg);
border-color: var(--active-bg);
color: white;
}

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