Compare commits

..

166 Commits

Author SHA1 Message Date
Mononaut
3b1f8ef4a2 fix custom x widget 2024-05-12 15:54:15 +00:00
Mononaut
86ef1e5fce update onbtc config 2024-05-12 15:47:00 +00:00
softsimon
b8725ab13a Merge pull request #5054 from mempool/mononaut/fix-statistics-graphs
Fix statistics graphs
2024-05-10 14:12:34 +07:00
wiz
6bf47f69eb Merge pull request #5056 from mempool/mononaut/unfurl-descriptions
temporarily hardcode unfurl titles & descriptions
2024-05-10 06:13:25 +09:00
wiz
0192c2bffa ops: Modify build script to build unfurlers last 2024-05-10 05:41:46 +09:00
Mononaut
91aa815d88 temporarily hardcode unfurl titles & descriptions 2024-05-09 20:01:13 +00:00
wiz
dcdb9361e8 Merge pull request #5055 from mempool/mononaut/custom-build-html
Update custom sv html
2024-05-10 04:22:15 +09:00
Mononaut
89531d1703 Update custom sv html 2024-05-09 19:18:17 +00:00
wiz
8311e842bb ops: Enable testnet4 in prod frontend configs 2024-05-10 04:02:35 +09:00
wiz
a492223369 Add warning that testnet4 may be reset at anytime 2024-05-10 03:57:25 +09:00
Mononaut
cd9e336165 Also fix stats on custom dashboard & tv view 2024-05-09 18:05:01 +00:00
Mononaut
5b2ecac1f6 Fix statistics graphs 2024-05-09 18:00:09 +00:00
wiz
d2f7864266 ops: Don't serve stale cache data for non-mainnet (2/2) 2024-05-10 01:14:51 +09:00
wiz
c9432aa0d1 Merge pull request #5053 from mempool/mononaut/unfurler-fixes-2
Fix unfurler resources build
2024-05-10 00:56:56 +09:00
Mononaut
eeb2bae9f3 Fix unfurler resources build 2024-05-09 15:55:03 +00:00
wiz
659ef0b6b9 ops: Don't serve stale cache data for non-mainnet 2024-05-10 00:44:53 +09:00
wiz
9b83ac29b7 Merge pull request #5050 from mempool/mononaut/t4-unfurls
Enable testnet4 unfurls
2024-05-09 23:42:39 +09:00
wiz
722b81623b Merge pull request #5051 from mempool/mononaut/fix-onbtc-fallback-img
Fix onbtc unfurler fallback image
2024-05-09 23:42:26 +09:00
wiz
648dd5adb2 Merge pull request #5052 from mempool/mononaut/testnet3-banner
Update testnet3 banner
2024-05-09 23:42:11 +09:00
Mononaut
75e7bb3255 Update testnet3 banner 2024-05-08 20:46:55 +00:00
Mononaut
10fc4d27ad Fix onbtc unfurler fallback image 2024-05-08 19:59:50 +00:00
Mononaut
ace422c0d5 Enable testnet4 unfurls 2024-05-08 19:58:57 +00:00
softsimon
b6682eed2a Merge pull request #5048 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.9.0
Bump cypress from 13.8.0 to 13.9.0 in /frontend
2024-05-09 00:09:55 +07:00
softsimon
8080b8b56a Remove Bukele theme option 2024-05-08 23:58:51 +07:00
softsimon
0e8d04e464 Merge pull request #5049 from mempool/natsoni/fix-navbar
Fix transparent navbar
2024-05-08 23:30:33 +07:00
natsoni
fc76146085 Fix transparent navbar 2024-05-08 18:25:29 +02:00
softsimon
4142413002 Merge pull request #5028 from mempool/mononaut/x-widget
Add x widget
2024-05-08 22:57:21 +07:00
softsimon
49e4855982 Merge branch 'master' into mononaut/x-widget 2024-05-08 22:56:03 +07:00
dependabot[bot]
65cad2025a Bump cypress from 13.8.0 to 13.9.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.8.0 to 13.9.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/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-08 15:29:38 +00:00
softsimon
64327b3d25 Merge pull request #5047 from mempool/dependabot/npm_and_yarn/frontend/esbuild-0.21.1
Bump esbuild from 0.20.2 to 0.21.1 in /frontend
2024-05-08 22:28:27 +07:00
wiz
54289368b1 Merge pull request #5027 from mempool/mononaut/tracker-logos
Add enterprise logos to pizza tracker header
2024-05-08 17:49:27 +09:00
softsimon
297c226fab Merge branch 'master' into mononaut/tracker-logos 2024-05-08 10:06:47 +07:00
softsimon
7f5b5f487e Merge pull request #5044 from mempool/mononaut/onbtc-imgs-themed
onbtc theme
2024-05-08 10:06:13 +07:00
softsimon
f2a6828015 Merge pull request #5045 from mempool/mononaut/fix-custom-dash-graph
Fix custom dashboard address graph
2024-05-08 10:01:31 +07:00
dependabot[bot]
45936805e9 Bump esbuild from 0.20.2 to 0.21.1 in /frontend
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.20.2 to 0.21.1.
- [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.20.2...v0.21.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-08 02:17:49 +00:00
Mononaut
0fae0b21ad Fix pizza tracker branding layout 2024-05-08 00:23:50 +00:00
Mononaut
9bd741b1d9 Add enterprise logos to pizza tracker header 2024-05-08 00:23:50 +00:00
Mononaut
c15393981d Update onbtc fallback preview img 2024-05-08 00:22:54 +00:00
Mononaut
9495d7d2ea Fix custom dashboard address graph 2024-05-08 00:17:35 +00:00
Mononaut
788a95b260 disable selector when forcing custom theme 2024-05-07 23:48:28 +00:00
Mononaut
4c697d0008 Update onbtc theme & branding 2024-05-07 23:48:23 +00:00
natsoni
463e66081c CSS theme changes 2024-05-07 21:03:26 +00:00
natsoni
a7e501570c Add El Salvador theme 2024-05-07 21:03:25 +00:00
natsoni
4a76cb083a More css included in color themes 2024-05-07 21:03:24 +00:00
natsoni
6a767ce5c4 Make links color manageable with themes 2024-05-07 21:02:40 +00:00
wiz
0fcd132df4 Merge pull request #5041 from mempool/mononaut/unbork-orphan-cache
Don't get stuck fetching orphans
2024-05-07 17:55:37 +09:00
Mononaut
0990cfe072 Don't get stuck fetching orphans 2024-05-07 01:41:41 +00:00
wiz
d92bf14b50 Merge pull request #5040 from mempool/mononaut/testnet4
test
2024-05-07 02:50:37 +09:00
Mononaut
de8462e81e testnet4 docs 2024-05-06 17:31:49 +00:00
Mononaut
15bd58c746 testnet4 beta tags 2024-05-06 17:31:16 +00:00
Mononaut
26da18810d Hide lightning tab on testnet4 2024-05-06 16:48:39 +00:00
wiz
f80c99576b ops: Update prod scripts for testnet4 2024-05-07 01:01:09 +09:00
Mononaut
1e5a55917c Add testnet4 frontend support 2024-05-06 15:55:49 +00:00
softsimon
375e43c191 Merge pull request #5038 from mempool/mononaut/improve-deferred-mempool-animations
Improve deferred mempool visualization updates
2024-05-06 22:40:12 +07:00
softsimon
feab4d2a51 Merge pull request #5035 from mempool/mononaut/accelerator-websocket
Replace acceleration API polling with websocket
2024-05-06 20:57:41 +07:00
softsimon
73f0cdde7d Merge pull request #5039 from mempool/dependabot/github_actions/dtolnay/rust-toolchain-d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a
Bump dtolnay/rust-toolchain from bb45937a053e097f8591208d8e74c90db1873d07 to d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a
2024-05-06 18:02:04 +07:00
softsimon
810c2cbde4 Merge pull request #5037 from mempool/nymkappa/node-socket
[lightning] apply #4991 at node creation as well
2024-05-06 17:59:40 +07:00
softsimon
a9ff9c3bc7 Merge pull request #5030 from TheBlueMatt/msrv-take-2
Restore MSRV to 1.63
2024-05-06 12:51:42 +07:00
dependabot[bot]
69faa1d493 Bump dtolnay/rust-toolchain
Bumps [dtolnay/rust-toolchain](https://github.com/dtolnay/rust-toolchain) from bb45937a053e097f8591208d8e74c90db1873d07 to d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a.
- [Release notes](https://github.com/dtolnay/rust-toolchain/releases)
- [Commits](bb45937a05...d8352f6b1d)

---
updated-dependencies:
- dependency-name: dtolnay/rust-toolchain
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 02:26:59 +00:00
Mononaut
a29b29300e Replace acceleration API polling with websocket 2024-05-04 17:23:20 +00:00
Mononaut
56a0d89b88 Improve deferred mempool visualization updates 2024-05-04 17:08:38 +00:00
nymkappa
2e49ef38c9 [lightning] apply 4991 at node creation as well 2024-05-04 10:01:21 +02:00
softsimon
8ec5dd70e0 Merge pull request #5026 from mempool/mononaut/balance-graph-span-toggle
Polish address balance graph, add period toggle
2024-05-04 14:31:26 +07:00
softsimon
89363bfbff Merge pull request #4815 from mempool/mononaut/testmempoolaccept
testmempoolaccept
2024-05-04 14:22:37 +07:00
softsimon
e45a03729b Adding footer link 2024-05-04 14:19:26 +07:00
softsimon
395ef82ad4 Merge branch 'master' into mononaut/testmempoolaccept 2024-05-04 14:11:48 +07:00
softsimon
02f1b11ad6 Merge pull request #5021 from mempool/dependabot/docker/docker/frontend/nginx-1.26.0-alpine
Bump nginx from 1.25.4-alpine to 1.26.0-alpine in /docker/frontend
2024-05-04 14:11:17 +07:00
softsimon
c53fb9266c Merge pull request #5023 from mempool/natsoni/block-view-fees-default
Change block display default mode to fees
2024-05-04 14:09:56 +07:00
softsimon
3d1edffd18 Merge pull request #5032 from mempool/mononaut/fix-mempool-animation-desync
Fix mempool animation desync
2024-05-04 14:08:42 +07:00
softsimon
24ea3199dd Merge pull request #4598 from mempool/mononaut/mempool-delta-subscription
Feature: track-mempool websocket subscriptions
2024-05-04 13:47:29 +07:00
Mononaut
44116424b0 Reimplement mempool animation smoothing within viz component 2024-05-03 21:43:53 +00:00
Mononaut
a8868b5f0f Simplify websocket mempool delta handling 2024-05-03 21:43:48 +00:00
Mononaut
5172f032e7 Add sequence number to track-mempool subscription messages 2024-05-03 16:31:56 +00:00
Mononaut
f8d30bf528 Add 'mempool delta' websocket subscriptions 2024-05-03 16:25:45 +00:00
softsimon
ba27ff9c42 Merge pull request #5022 from mempool/dependabot/npm_and_yarn/backend/ws-8.17.0
Bump ws from 8.16.0 to 8.17.0 in /backend
2024-05-03 05:16:37 +07:00
softsimon
02878d6a58 Merge pull request #5029 from mempool/dependabot/npm_and_yarn/unfurler/ejs-3.1.10
Bump ejs from 3.1.9 to 3.1.10 in /unfurler
2024-05-03 05:15:15 +07:00
Matt Corallo
a676d23a54 Restore MSRV to 1.63
In trying to upgrade my mempool instance, I discovered I couldn't
build the latest mempool Rust code with my available Rust
toolchain.

It appears in #4612 the Rust MSRV was bumped without justification,
which is reverted here. Note that `Cargo.lock` updates here should
ensure the versions of dependent crates use the versions supported
by our MSRV.

Its possible that the dependency downgrades here break something,
but things appear to be running fine for me locally, so figured I'd
suggest this upstream.
2024-05-02 21:00:49 +00:00
dependabot[bot]
36f1f778c3 Bump ejs from 3.1.9 to 3.1.10 in /unfurler
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 18:34:47 +00:00
Mononaut
f83404421a Add x widget 2024-05-01 23:59:02 +00:00
wiz
cd8abe5c06 ops: Tweak http port for onbtc unfurler instance 2024-05-02 00:53:14 +09:00
softsimon
471a2397c0 Add customize.js to .gitignore 2024-05-01 22:51:58 +07:00
Mononaut
a582a3b0ed Polish address balance graph, add period toggle 2024-04-29 21:03:09 +00:00
softsimon
20df70c449 Fix missing return on invalid params 2024-04-30 02:02:53 +07:00
wiz
af13b6c9f8 Merge pull request #5024 from mempool/mononaut/onbtc-siteid
Add onbtc site id
2024-04-30 01:33:36 +09:00
Mononaut
06ee82af0b Add onbtc site id 2024-04-29 14:36:45 +00:00
natsoni
57b466d80f Update block display default mode to fees 2024-04-29 11:54:28 +02:00
wiz
dbd4d152ce Merge pull request #5020 from mempool/mononaut/custom-unfurls
custom unfurls
2024-04-29 17:37:11 +09:00
wiz
95f47409d8 Add onbtc frontend config, add onbtc backend to build script 2024-04-29 17:29:45 +09:00
wiz
ed64b72676 ops: Add onbtc frontend config 2024-04-29 17:20:35 +09:00
wiz
f9948055e4 ops: Update build/unfurler stuff for onbtc 2024-04-29 17:12:51 +09:00
dependabot[bot]
6ad709f93a Bump ws from 8.16.0 to 8.17.0 in /backend
Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.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-04-29 02:16:33 +00:00
dependabot[bot]
f9498cf5a2 Bump nginx from 1.25.4-alpine to 1.26.0-alpine in /docker/frontend
Bumps nginx from 1.25.4-alpine to 1.26.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-04-29 02:06:21 +00:00
Mononaut
f5333c529c Update unfurler routing, add fallback img 2024-04-28 16:18:35 +00:00
Mononaut
6b2cc23cd3 Add custom branding to unfurler previews 2024-04-28 16:18:16 +00:00
Mononaut
34329d04f0 favicons 2024-04-28 16:02:42 +00:00
Mononaut
701c283e97 Fix svg 2024-04-28 16:02:41 +00:00
Mononaut
f9fd589af2 Logo overrides, custom index.html 2024-04-28 16:02:41 +00:00
softsimon
0d9602693b Correcting duplicate string 'Date' 2024-04-28 09:46:55 +07:00
softsimon
27be568eb5 Fix duplicate "sats" string 2024-04-28 09:43:35 +07:00
softsimon
8f06aea736 Extracting new i18n strings 2024-04-28 09:39:04 +07:00
wiz
65191538e2 Merge pull request #5016 from mempool/mononaut/customization
Customized frontend builds
2024-04-28 04:11:11 +09:00
Mononaut
d49485c363 Update custom address widgets on new block transactions 2024-04-26 23:59:27 +00:00
Mononaut
1de028840e Custom address widget title links 2024-04-26 20:06:34 +00:00
Mononaut
3f9fb68c5f Fix mobile custom enterprise logo 2024-04-26 20:01:00 +00:00
Mononaut
33ef15511f Check in missing address transactions widget component 2024-04-26 19:41:11 +00:00
Mononaut
139c384e97 Add address transactions widget 2024-04-26 19:17:28 +00:00
Mononaut
ccb27dbdb9 Address widget timespans 2024-04-26 19:17:27 +00:00
Mononaut
6eb21ffd24 check in svg 2024-04-26 19:17:27 +00:00
Mononaut
b9b3c8f78a Custom branding 2024-04-26 19:17:27 +00:00
Mononaut
4f4215577a Add custom dashboard component 2024-04-26 19:17:27 +00:00
Mononaut
ac0f56325b Add customizable enterprise build 2024-04-26 19:17:27 +00:00
softsimon
fd8df2a035 Merge pull request #5015 from mempool/natsoni/add-toggle-sats
Add an option to display values as sats
2024-04-26 12:58:52 +07:00
softsimon
b4decb8f91 Merge pull request #5014 from mempool/natsoni/small-fixes-liquid-frontend
Minor changes on Liquid frontend
2024-04-26 12:07:07 +07:00
softsimon
836c1e002e Merge pull request #4981 from mempool/natsoni/fix-tx-details-loading
Fix miner data loading in transaction details
2024-04-26 12:06:03 +07:00
softsimon
701701e13c Merge pull request #5013 from mempool/natsoni/fix-liquid-footer
Remove Mining dashboard from Liquid footer
2024-04-26 11:54:36 +07:00
natsoni
ea5c3212d6 Add an option to display values as sats 2024-04-24 16:44:56 +02:00
natsoni
65705a8a1d Fix Liquid UTXOs table color 2024-04-24 14:19:14 +02:00
natsoni
8310ad9f24 Fix gauge clickable pointer 2024-04-24 14:17:50 +02:00
natsoni
1a5c2c4d3a Add a 5% tolerance for declaring a "unpeg event" 2024-04-24 14:17:40 +02:00
natsoni
cd689afaaa Add config check to show mining dashboard in footer 2024-04-24 11:00:51 +02:00
softsimon
00e0eea60e Merge pull request #5012 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.9.7
Bump mysql2 from 3.9.4 to 3.9.7 in /backend
2024-04-24 10:38:42 +07:00
softsimon
d89ba767f1 Merge pull request #5011 from mempool/mononaut/rbf-mobile-autoscroll-bug
Fix rbf autoscroll bug on mobile
2024-04-24 10:34:34 +07:00
dependabot[bot]
c4acc95b06 Bump mysql2 from 3.9.4 to 3.9.7 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.9.4 to 3.9.7.
- [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.4...v3.9.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-23 23:11:18 +00:00
nymkappa
ed9f792d21 fix typo 2024-04-23 17:54:04 +02:00
Mononaut
31c008afa5 Fix rbf autoscroll bug on mobile 2024-04-22 21:52:48 +00:00
softsimon
a66920c51e Merge pull request #5004 from mempool/natsoni/toggle-block-fees
Add block display mode toggle
2024-04-22 20:30:26 +07:00
softsimon
1c9e2b546b Replacing setTimeouts with rxjs 2024-04-22 14:42:48 +07:00
softsimon
2297ff72f5 Merge pull request #5006 from mempool/knorrium/increase_language_selector_width
Knorrium/increase language selector width
2024-04-22 10:24:38 +07:00
Felipe Knorr Kuhn
fbaa39a300 Change only the selector width 2024-04-21 19:34:48 -07:00
Felipe Knorr Kuhn
bd2c15c6bb Increase the language selector width to accomodate all languages 2024-04-21 18:48:44 -07:00
natsoni
a4d8f2db58 Add block display mode toggle button 2024-04-21 14:54:50 +02:00
Mononaut
2a43255802 testmempool accept more validation & switch to JSON array format 2024-04-21 10:05:58 +07:00
Mononaut
f3232b2d5c Add Test Transactions page 2024-04-21 10:05:58 +07:00
Mononaut
c7cb7d1ac4 Add testmempoolaccept API endpoint 2024-04-21 10:05:57 +07:00
softsimon
bd5a23ff0d Merge pull request #4997 from mempool/natsoni/fix-dropdown-focus
Fix dropdown menu navigation with arrow keys
2024-04-20 08:35:17 +07:00
natsoni
dd0c1eb74d Fix dropdown menu arrow navigation 2024-04-20 00:57:59 +02:00
softsimon
9950ef16ce Merge pull request #4995 from mempool/natsoni/fix-pagination-color
Fix table pagination colors on custom themes
2024-04-20 01:25:56 +07:00
softsimon
fed2c034ee Merge pull request #4996 from mempool/nymkappa/update-doc
[doc] update accel doc
2024-04-20 00:47:03 +07:00
nymkappa
4944e3c257 [doc] update accel doc 2024-04-19 18:45:59 +02:00
natsoni
b865fa33f6 Fix table pagination colors 2024-04-19 17:08:19 +02:00
softsimon
bb44ed15da Merge pull request #4991 from mempool/nymkappa/unique-node-addr
[lightning] unique sockets for ln nodes
2024-04-19 18:02:42 +07:00
softsimon
93919421ee Merge pull request #4984 from mempool/natsoni/more-css
More css fixes
2024-04-19 16:24:25 +07:00
softsimon
8d1831129b Merge pull request #4988 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.8.0
Bump cypress from 13.7.0 to 13.8.0 in /frontend
2024-04-19 16:23:30 +07:00
softsimon
5e6e2c037e Merge pull request #4979 from mempool/natsoni/improve-tooltip-positioning
Fix cropped transaction tooltip in block overview on mobile
2024-04-19 16:23:10 +07:00
nymkappa
ddfceddc57 [lightning] unique sockets for ln nodes 2024-04-19 10:56:15 +02:00
softsimon
5b19ffd524 Pull from transifex 2024-04-19 14:53:32 +07:00
wiz
7422001ed5 Merge pull request #4990 from mempool/nymkappa/disable-topology-prod
[prod] disable LN TOPOLOGY_FOLDER by default
2024-04-19 16:27:16 +09:00
nymkappa
3b0ef48cdf [prod] disable LN TOPOLOGY_FOLDER by default 2024-04-19 09:06:10 +02:00
wiz
d13875e3e7 Merge pull request #4989 from mempool/nymkappa/update-doc
[doc] update
2024-04-19 16:06:03 +09:00
nymkappa
6ebc7fab47 [doc] update to match accel history endpoint actual response 2024-04-19 08:55:21 +02:00
dependabot[bot]
9d931c1d13 Bump cypress from 13.7.0 to 13.8.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.7.0 to 13.8.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.7.0...v13.8.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-04-19 02:15:31 +00:00
softsimon
f86b6ac3de Merge pull request #4985 from mempool/mononaut/fix-audit-bug
Fix audit blockIndex increment bug
2024-04-19 00:13:48 +07:00
natsoni
ce1124284e Adapt more css for color theme 2024-04-18 16:18:38 +02:00
Mononaut
233f19d23d Fix audit blockIndex increment bug 2024-04-18 13:27:33 +00:00
natsoni
89ef5fe33d Fix tooltip text overflow in some languages 2024-04-18 11:09:28 +02:00
softsimon
7cd4345264 Merge pull request #4956 from henrialb/typo
Fix typo in docs
2024-04-18 11:59:57 +07:00
natsoni
bbaa5dafb9 Separate mining and audit subscriptions 2024-04-17 16:41:30 +02:00
wiz
cee1b39640 Merge pull request #4980 from mempool/mononaut/fix-acc-button-conditions 2024-04-17 04:06:27 +09:00
Mononaut
e388410e34 Re-add accelerate button on deep mempool txs 2024-04-16 18:52:13 +00:00
natsoni
c2d649d3f4 Improve tooltip position in block overview on mobile 2024-04-16 16:02:39 +02:00
wiz
d9dec44c8f Merge pull request #4977 from mempool/nymkappa/fix-copy
[accelerator] fix pizza accel copy/eta
2024-04-16 21:14:56 +09:00
wiz
c94a3a98eb Change "Settlement" to "Confirmation" 2024-04-16 21:14:45 +09:00
nymkappa
ec9b8359df [accelerator] fix pizza accel copy/eta x2 2024-04-16 21:12:25 +09:00
nymkappa
3db89e3dee [accelerator] fix pizza accel copy/eta 2024-04-16 21:09:30 +09:00
Henrique Albuquerque
f89bd5b972 Add contributor statement 2024-04-12 18:05:18 +01:00
Henrique Albuquerque
d94a8cb58c Fix typo 2024-04-12 17:59:31 +01:00
220 changed files with 7396 additions and 1498 deletions

View File

@@ -35,7 +35,7 @@ jobs:
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
# Latest version available on this commit is 1.71.1
# Commit date is Aug 3, 2023
uses: dtolnay/rust-toolchain@bb45937a053e097f8591208d8e74c90db1873d07
uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a
with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}

View File

@@ -18,12 +18,12 @@
"crypto-js": "~4.2.0",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.9.4",
"mysql2": "~3.9.7",
"redis": "^4.6.6",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.16.0"
"ws": "~8.17.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.4",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz",
"integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==",
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -7690,9 +7690,9 @@
}
},
"node_modules/ws": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"engines": {
"node": ">=10.0.0"
},
@@ -12382,9 +12382,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz",
"integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==",
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
"requires": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -13424,9 +13424,9 @@
}
},
"ws": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"requires": {}
},
"y18n": {

View File

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

View File

@@ -75,10 +75,6 @@ class Audit {
let failures = 0;
let blockIndex = 1;
while (projectedBlocks[blockIndex] && failures < 500) {
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
index = 0;
blockIndex++;
}
const txid = projectedBlocks[blockIndex].transactionIds[index];
const tx = mempool[txid];
if (tx) {
@@ -102,6 +98,10 @@ class Audit {
logger.warn('projected transaction missing from mempool cache');
}
index++;
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
index = 0;
blockIndex++;
}
}
// mark unexpected transactions in the mined block as 'added'

View File

@@ -1,4 +1,4 @@
import { IBitcoinApi } from './bitcoin-api.interface';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
@@ -22,6 +22,7 @@ export interface AbstractBitcoinApi {
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;

View File

@@ -205,3 +205,16 @@ export namespace IBitcoinApi {
"utxo_size_inc": number;
}
}
export interface TestMempoolAcceptResult {
txid: string,
wtxid: string,
allowed?: boolean,
vsize?: number,
fees?: {
base: number,
"effective-feerate": number,
"effective-includes": string[],
},
['reject-reason']?: string,
}

View File

@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import mempool from '../mempool';
@@ -174,6 +174,14 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.sendRawTransaction(rawTransaction);
}
async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
if (rawTransactions.length) {
return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined);
} else {
return [];
}
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return {

View File

@@ -55,6 +55,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/test', this.$testTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
@@ -749,6 +750,19 @@ class BitcoinRoutes {
}
}
private async $testTransactions(req: Request, res: Response) {
try {
const rawTxs = Common.getTransactionsFromRequest(req);
const maxfeerate = parseFloat(req.query.maxfeerate as string);
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result);
} catch (e: any) {
res.setHeader('content-type', 'text/plain');
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
}
export default new BitcoinRoutes();

View File

@@ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
import { Common } from '../common';
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
interface FailoverHost {
host: string,
@@ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.');
}
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
throw new Error('Method not implemented.');
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
}

View File

@@ -839,8 +839,11 @@ class Blocks {
} else {
this.currentBlockHeight++;
logger.debug(`New block found (#${this.currentBlockHeight})!`);
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
await chainTips.updateOrphanedBlocks();
// skip updating the orphan block cache if we've fallen behind the chain tip
if (this.currentBlockHeight >= blockHeightTip - 2) {
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
await chainTips.updateOrphanedBlocks();
}
}
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);

View File

@@ -12,32 +12,68 @@ export interface OrphanedBlock {
height: number;
hash: string;
status: 'valid-fork' | 'valid-headers' | 'headers-only';
prevhash: string;
}
class ChainTips {
private chainTips: ChainTip[] = [];
private orphanedBlocks: OrphanedBlock[] = [];
private orphanedBlocks: { [hash: string]: OrphanedBlock } = {};
private blockCache: { [hash: string]: OrphanedBlock } = {};
private orphansByHeight: { [height: number]: OrphanedBlock[] } = {};
public async updateOrphanedBlocks(): Promise<void> {
try {
this.chainTips = await bitcoinClient.getChainTips();
this.orphanedBlocks = [];
const start = Date.now();
const breakAt = start + 10000;
let newOrphans = 0;
this.orphanedBlocks = {};
for (const chain of this.chainTips) {
if (chain.status === 'valid-fork' || chain.status === 'valid-headers') {
let block = await bitcoinClient.getBlock(chain.hash);
while (block && block.confirmations === -1) {
this.orphanedBlocks.push({
height: block.height,
hash: block.hash,
status: chain.status
});
block = await bitcoinClient.getBlock(block.previousblockhash);
const orphans: OrphanedBlock[] = [];
let hash = chain.hash;
do {
let orphan = this.blockCache[hash];
if (!orphan) {
const block = await bitcoinClient.getBlock(hash);
if (block && block.confirmations === -1) {
newOrphans++;
orphan = {
height: block.height,
hash: block.hash,
status: chain.status,
prevhash: block.previousblockhash,
};
this.blockCache[hash] = orphan;
}
}
if (orphan) {
orphans.push(orphan);
}
hash = orphan?.prevhash;
} while (hash && (Date.now() < breakAt));
for (const orphan of orphans) {
this.orphanedBlocks[orphan.hash] = orphan;
}
}
if (Date.now() >= breakAt) {
logger.debug(`Breaking orphaned blocks updater after 10s, will continue next block`);
break;
}
}
logger.debug(`Updated orphaned blocks cache. Found ${this.orphanedBlocks.length} orphaned blocks`);
this.orphansByHeight = {};
const allOrphans = Object.values(this.orphanedBlocks);
for (const orphan of allOrphans) {
if (!this.orphansByHeight[orphan.height]) {
this.orphansByHeight[orphan.height] = [];
}
this.orphansByHeight[orphan.height].push(orphan);
}
logger.debug(`Updated orphaned blocks cache. Fetched ${newOrphans} new orphaned blocks. Total ${allOrphans.length}`);
} catch (e) {
logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`);
}
@@ -48,13 +84,7 @@ class ChainTips {
return [];
}
const orphans: OrphanedBlock[] = [];
for (const block of this.orphanedBlocks) {
if (block.height === height) {
orphans.push(block);
}
}
return orphans;
return this.orphansByHeight[height] || [];
}
}

View File

@@ -946,6 +946,33 @@ export class Common {
return this.validateTransactionHex(matches[1].toLowerCase());
}
static getTransactionsFromRequest(req: Request, limit: number = 25): string[] {
if (!Array.isArray(req.body) || req.body.some(hex => typeof hex !== 'string')) {
throw Object.assign(new Error('Invalid request body (should be an array of hexadecimal strings)'), { code: -1 });
}
if (limit && req.body.length > limit) {
throw Object.assign(new Error('Exceeded maximum of 25 transactions'), { code: -1 });
}
const txs = req.body;
return txs.map(rawTx => {
// Support both upper and lower case hex
// Support both txHash= Form and direct API POST
const reg = /^((?:[a-fA-F0-9]{2})+)$/;
const matches = reg.exec(rawTx);
if (!matches || !matches[1]) {
throw Object.assign(new Error('Invalid hex string'), { code: -2 });
}
// Guaranteed to be a hex string of multiple of 2
// Guaranteed to be lower case
// Guaranteed to pass validation (see function below)
return this.validateTransactionHex(matches[1].toLowerCase());
});
}
private static validateTransactionHex(txhex: string): string {
// Do not mutate txhex

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 81;
private static currentVersion = 78;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -652,6 +652,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `prices` ADD `THB` float DEFAULT "-1"');
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
await this.updateToSchemaVersion(75);
}
@@ -669,21 +674,6 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `prices` CHANGE `time` `time` datetime NOT NULL');
await this.updateToSchemaVersion(78);
}
if (databaseSchemaVersion < 79 && isBitcoin === true) {
await this.$executeQuery('TRUNCATE hashrates');
await this.updateToSchemaVersion(79);
}
if (databaseSchemaVersion < 80 && isBitcoin === true) {
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.updateToSchemaVersion(80);
}
if (databaseSchemaVersion < 81 && isBitcoin === true) {
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
await this.updateToSchemaVersion(81);
}
}
/**

View File

@@ -54,9 +54,11 @@ class ChannelsRoutes {
if (index < -1) {
res.status(400).send('Invalid index');
return;
}
if (['open', 'active', 'closed'].includes(status) === false) {
res.status(400).send('Invalid status');
return;
}
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);

View File

@@ -666,7 +666,9 @@ class NodesApi {
node.last_update = null;
}
const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? '';
const uniqueAddr = [...new Set(node.addresses?.map(a => a.addr))];
const formattedSockets = (uniqueAddr.join(',')) ?? '';
const query = `INSERT INTO nodes(
public_key,
first_seen,
@@ -695,13 +697,13 @@ class NodesApi {
node.alias,
this.aliasToSearchText(node.alias),
node.color,
sockets,
formattedSockets,
JSON.stringify(node.features),
node.last_update,
node.alias,
this.aliasToSearchText(node.alias),
node.color,
sockets,
formattedSockets,
JSON.stringify(node.features),
]);
} catch (e) {
@@ -713,7 +715,9 @@ class NodesApi {
* Update node sockets
*/
public async $updateNodeSockets(publicKey: string, sockets: {network: string; addr: string}[]): Promise<void> {
const formattedSockets = (sockets.map(a => a.addr).join(',')) ?? '';
const uniqueAddr = [...new Set(sockets.map(a => a.addr))];
const formattedSockets = (uniqueAddr.join(',')) ?? '';
try {
await DB.query(`UPDATE nodes SET sockets = ? WHERE public_key = ?`, [formattedSockets, publicKey]);
} catch (e) {

View File

@@ -404,6 +404,10 @@ class Mempool {
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration

View File

@@ -3,6 +3,7 @@ import * as WebSocket from 'ws';
import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -346,6 +347,17 @@ class WebsocketHandler {
}
}
if (parsedMessage && parsedMessage['track-accelerations'] != null) {
if (parsedMessage['track-accelerations']) {
client['track-accelerations'] = true;
response['accelerations'] = JSON.stringify({
accelerations: Object.values(memPool.getAccelerations()),
});
} else {
client['track-accelerations'] = false;
}
}
if (parsedMessage.action === 'init') {
if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) {
this.updateSocketData();
@@ -364,6 +376,18 @@ class WebsocketHandler {
client['track-donation'] = parsedMessage['track-donation'];
}
if (parsedMessage['track-mempool-txids'] === true) {
client['track-mempool-txids'] = true;
} else if (parsedMessage['track-mempool-txids'] === false) {
delete client['track-mempool-txids'];
}
if (parsedMessage['track-mempool'] === true) {
client['track-mempool'] = true;
} else if (parsedMessage['track-mempool'] === false) {
delete client['track-mempool'];
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
@@ -524,6 +548,7 @@ class WebsocketHandler {
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements;
@@ -545,6 +570,33 @@ class WebsocketHandler {
const latestTransactions = memPool.getLatestTransactions();
if (memPool.isInSync()) {
this.mempoolSequence++;
}
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid]) {
replacedTransactions.push({ replaced: replaced.txid, by: tx });
}
}
}
const mempoolDeltaTxids: MempoolDeltaTxids = {
sequence: this.mempoolSequence,
added: newTransactions.map(tx => tx.txid),
removed: deletedTransactions.map(tx => tx.txid),
mined: [],
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
};
const mempoolDelta: MempoolDelta = {
sequence: this.mempoolSequence,
added: newTransactions,
removed: deletedTransactions.map(tx => tx.txid),
mined: [],
replaced: replacedTransactions,
};
// update init data
const socketDataFields = {
'mempoolInfo': mempoolInfo,
@@ -604,9 +656,11 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
if (memPool.isInSync()) {
this.mempoolSequence++;
}
// pre-compute acceleration delta
const accelerationUpdate = {
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]),
};
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
@@ -847,6 +901,18 @@ class WebsocketHandler {
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
}
if (client['track-mempool-txids']) {
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
}
if (client['track-mempool']) {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
}
if (client['track-accelerations'] && (accelerationUpdate.added.length || accelerationUpdate.removed.length)) {
response['accelerations'] = getCachedResponse('accelerations', accelerationUpdate);
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
@@ -992,6 +1058,31 @@ class WebsocketHandler {
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
if (memPool.isInSync()) {
this.mempoolSequence++;
}
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const txid of Object.keys(rbfTransactions)) {
for (const replaced of rbfTransactions[txid].replaced) {
replacedTransactions.push({ replaced: replaced.txid, by: rbfTransactions[txid].replacedBy });
}
}
const mempoolDeltaTxids: MempoolDeltaTxids = {
sequence: this.mempoolSequence,
added: [],
removed: [],
mined: transactions.map(tx => tx.txid),
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
};
const mempoolDelta: MempoolDelta = {
sequence: this.mempoolSequence,
added: [],
removed: [],
mined: transactions.map(tx => tx.txid),
replaced: replacedTransactions,
};
const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string {
if (!responseCache[key]) {
@@ -1000,10 +1091,6 @@ class WebsocketHandler {
return responseCache[key];
}
if (memPool.isInSync()) {
this.mempoolSequence++;
}
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
@@ -1185,6 +1272,14 @@ class WebsocketHandler {
}
}
if (client['track-mempool-txids']) {
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
}
if (client['track-mempool']) {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}

View File

@@ -131,6 +131,7 @@ class Server {
})
.use(express.urlencoded({ extended: true }))
.use(express.text({ type: ['text/plain', 'application/base64'] }))
.use(express.json())
;
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {

View File

@@ -71,6 +71,22 @@ export interface MempoolBlockDelta {
changed: MempoolDeltaChange[];
}
export interface MempoolDeltaTxids {
sequence: number,
added: string[];
removed: string[];
mined: string[];
replaced: { replaced: string, by: string }[];
}
export interface MempoolDelta {
sequence: number,
added: MempoolTransactionExtended[];
removed: string[];
mined: string[];
replaced: { replaced: string, by: TransactionExtended }[];
}
interface VinStrippedToScriptsig {
scriptsig: string;
}

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 April 12, 2024.
Signed: henrialb

View File

@@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
RUN npm run build
FROM nginx:1.25.4-alpine
FROM nginx:1.26.0-alpine
WORKDIR /patch

1
frontend/.gitignore vendored
View File

@@ -63,6 +63,7 @@ src/resources/pools.json
src/resources/mining-pools/*
src/resources/**/*.mp4
src/resources/**/*.vtt
src/resources/customize.js
# environment config
mempool-frontend-config.json

View File

@@ -166,6 +166,7 @@
"src/resources",
"src/robots.txt",
"src/config.js",
"src/customize.js",
"src/config.template.js"
],
"styles": [
@@ -180,6 +181,11 @@
"bundleName": "wiz",
"inject": false
},
{
"input": "src/theme-bukele.scss",
"bundleName": "bukele",
"inject": false
},
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
],
"vendorChunk": true,

View File

@@ -0,0 +1,52 @@
{
"theme": "bukele",
"enterprise": "onbtc",
"branding": {
"name": "onbtc",
"title": "Bitcoin Office",
"site_id": 19,
"header_img": "/resources/onbtclogo.svg",
"footer_img": "/resources/onbtclogo.svg",
"rounded_corner": true
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "balance",
"mobileOrder": 1,
"props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
}
},
{
"component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "nayibbukele"
}
},
{
"component": "address",
"mobileOrder": 2,
"props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
"period": "1m"
}
},
{
"component": "blocks"
},
{
"component": "addressTransactions",
"mobileOrder": 3,
"props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
}
}
]
}
}

View File

@@ -4,11 +4,13 @@ const { spawnSync } = require('child_process');
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js';
const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js';
const GENERATED_CUSTOMIZATION_FILE_NAME = 'src/resources/customize.js';
let settings = [];
let configContent = {};
let gitCommitHash = '';
let packetJsonVersion = '';
let customConfig;
try {
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
@@ -22,7 +24,13 @@ try {
}
}
const indexFilePath = configContent.BASE_MODULE ? 'src/index.' + configContent.BASE_MODULE + '.html' : 'src/index.mempool.html';
if (configContent && configContent.CUSTOMIZATION) {
customConfig = readConfig(configContent.CUSTOMIZATION);
}
const baseModuleName = configContent.BASE_MODULE || 'mempool';
const customBuildName = (customConfig && configContent.enterprise) ? ('.' + configContent.enterprise) : '';
const indexFilePath = 'src/index.' + baseModuleName + customBuildName + '.html';
try {
fs.copyFileSync(indexFilePath, 'src/index.html');
@@ -109,6 +117,17 @@ writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate);
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
let customConfigJs = '';
if (customConfig) {
console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`);
customConfigJs = `(function (window) {
window.__env = window.__env || {};
window.__env.customize = ${customConfig};
}((typeof global !== 'undefined') ? global : this));
`;
}
writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs);
if (currentConfig && currentConfig === newConfig) {
console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`);
return;

View File

@@ -32,9 +32,10 @@
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"cypress": "^13.9.0",
"domino": "^2.1.6",
"echarts": "~5.5.0",
"esbuild": "^0.20.2",
"esbuild": "^0.21.1",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.1.0",
"ngx-infinite-scroll": "^17.0.0",
@@ -62,7 +63,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.7.0",
"cypress": "^13.9.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
@@ -3196,9 +3197,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
"integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
"cpu": [
"ppc64"
],
@@ -3211,9 +3212,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"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==",
"cpu": [
"arm"
],
@@ -3226,9 +3227,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
"integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
"cpu": [
"arm64"
],
@@ -3241,9 +3242,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
"integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
"cpu": [
"x64"
],
@@ -3256,9 +3257,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
"integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
"cpu": [
"arm64"
],
@@ -3271,9 +3272,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"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==",
"cpu": [
"x64"
],
@@ -3286,9 +3287,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
"integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
"cpu": [
"arm64"
],
@@ -3301,9 +3302,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
"integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
"cpu": [
"x64"
],
@@ -3316,9 +3317,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
"integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
"cpu": [
"arm"
],
@@ -3331,9 +3332,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
"integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
"cpu": [
"arm64"
],
@@ -3346,9 +3347,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"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==",
"cpu": [
"ia32"
],
@@ -3361,9 +3362,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
"integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
"cpu": [
"loong64"
],
@@ -3376,9 +3377,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
"integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
"cpu": [
"mips64el"
],
@@ -3391,9 +3392,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
"integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
"cpu": [
"ppc64"
],
@@ -3406,9 +3407,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
"integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
"cpu": [
"riscv64"
],
@@ -3421,9 +3422,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
"integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
"cpu": [
"s390x"
],
@@ -3436,9 +3437,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
"integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
"cpu": [
"x64"
],
@@ -3451,9 +3452,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
"integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
"cpu": [
"x64"
],
@@ -3466,9 +3467,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"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==",
"cpu": [
"x64"
],
@@ -3481,9 +3482,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
"integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
"cpu": [
"x64"
],
@@ -3496,9 +3497,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"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==",
"cpu": [
"arm64"
],
@@ -3511,9 +3512,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
"integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
"cpu": [
"ia32"
],
@@ -3526,9 +3527,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
"integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
"cpu": [
"x64"
],
@@ -8028,9 +8029,9 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.0.tgz",
"integrity": "sha512-UimjRSJJYdTlvkChcdcfywKJ6tUYuwYuk/n1uMMglrvi+ZthNhoRYcxnWgTqUtkl17fXrPAsD5XT2rcQYN1xKA==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
"integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@@ -9196,9 +9197,9 @@
}
},
"node_modules/esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
"integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@@ -9207,29 +9208,29 @@
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
"@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"
}
},
"node_modules/esbuild-wasm": {
@@ -20562,141 +20563,141 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="
},
"@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.1.tgz",
"integrity": "sha512-O7yppwipkXvnEPjzkSXJRk2g4bS8sUx9p9oXHq9MU/U7lxUzZVsnFZMDTmeeX9bfQxrFcvOacl/ENgOh0WP9pA==",
"optional": true
},
"@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"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==",
"optional": true
},
"@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.1.tgz",
"integrity": "sha512-jXhccq6es+onw7x8MxoFnm820mz7sGa9J14kLADclmiEUH4fyj+FjR6t0M93RgtlI/awHWhtF0Wgfhqgf9gDZA==",
"optional": true
},
"@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.1.tgz",
"integrity": "sha512-NPObtlBh4jQHE01gJeucqEhdoD/4ya2owSIS8lZYS58aR0x7oZo9lB2lVFxgTANSa5MGCBeoQtr+yA9oKCGPvA==",
"optional": true
},
"@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.1.tgz",
"integrity": "sha512-BLT7TDzqsVlQRmJfO/FirzKlzmDpBWwmCUlyggfzUwg1cAxVxeA4O6b1XkMInlxISdfPAOunV9zXjvh5x99Heg==",
"optional": true
},
"@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"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==",
"optional": true
},
"@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.1.tgz",
"integrity": "sha512-/uVdqqpNKXIxT6TyS/oSK4XE4xWOqp6fh4B5tgAwozkyWdylcX+W4YF2v6SKsL4wCQ5h1bnaSNjWPXG/2hp8AQ==",
"optional": true
},
"@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.1.tgz",
"integrity": "sha512-paAkKN1n1jJitw+dAoR27TdCzxRl1FOEITx3h201R6NoXUojpMzgMLdkXVgCvaCSCqwYkeGLoe9UVNRDKSvQgw==",
"optional": true
},
"@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.1.tgz",
"integrity": "sha512-tRHnxWJnvNnDpNVnsyDhr1DIQZUfCXlHSCDohbXFqmg9W4kKR7g8LmA3kzcwbuxbRMKeit8ladnCabU5f2traA==",
"optional": true
},
"@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.1.tgz",
"integrity": "sha512-G65d08YoH00TL7Xg4LaL3gLV21bpoAhQ+r31NUu013YB7KK0fyXIt05VbsJtpqh/6wWxoLJZOvQHYnodRrnbUQ==",
"optional": true
},
"@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"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==",
"optional": true
},
"@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.1.tgz",
"integrity": "sha512-MhNalK6r0nZD0q8VzUBPwheHzXPr9wronqmZrewLfP7ui9Fv1tdPmg6e7A8lmg0ziQCziSDHxh3cyRt4YMhGnQ==",
"optional": true
},
"@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.1.tgz",
"integrity": "sha512-YCKVY7Zen5rwZV+nZczOhFmHaeIxR4Zn3jcmNH53LbgF6IKRwmrMywqDrg4SiSNApEefkAbPSIzN39FC8VsxPg==",
"optional": true
},
"@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.1.tgz",
"integrity": "sha512-bw7bcQ+270IOzDV4mcsKAnDtAFqKO0jVv3IgRSd8iM0ac3L8amvCrujRVt1ajBTJcpDaFhIX+lCNRKteoDSLig==",
"optional": true
},
"@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.1.tgz",
"integrity": "sha512-ARmDRNkcOGOm1AqUBSwRVDfDeD9hGYRfkudP2QdoonBz1ucWVnfBPfy7H4JPI14eYtZruRSczJxyu7SRYDVOcg==",
"optional": true
},
"@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.1.tgz",
"integrity": "sha512-o73TcUNMuoTZlhwFdsgr8SfQtmMV58sbgq6gQq9G1xUiYnHMTmJbwq65RzMx89l0iya69lR4bxBgtWiiOyDQZA==",
"optional": true
},
"@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.1.tgz",
"integrity": "sha512-da4/1mBJwwgJkbj4fMH7SOXq2zapgTo0LKXX1VUZ0Dxr+e8N0WbS80nSZ5+zf3lvpf8qxrkZdqkOqFfm57gXwA==",
"optional": true
},
"@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.1.tgz",
"integrity": "sha512-CPWs0HTFe5woTJN5eKPvgraUoRHrCtzlYIAv9wBC+FAyagBSaf+UdZrjwYyTGnwPGkThV4OCI7XibZOnPvONVw==",
"optional": true
},
"@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"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==",
"optional": true
},
"@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.1.tgz",
"integrity": "sha512-CWibXszpWys1pYmbr9UiKAkX6x+Sxw8HWtw1dRESK1dLW5fFJ6rMDVw0o8MbadusvVQx1a8xuOxnHXT941Hp1A==",
"optional": true
},
"@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"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==",
"optional": true
},
"@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.1.tgz",
"integrity": "sha512-PgyFvjJhXqHn1uxPhyN1wZ6dIomKjiLUQh1LjFvjiV1JmnkZ/oMPrfeEAZg5R/1ftz4LZWZr02kefNIQ5SKREQ==",
"optional": true
},
"@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.1.tgz",
"integrity": "sha512-W9NttRZQR5ehAiqHGDnvfDaGmQOm6Fi4vSlce8mjM75x//XKuVAByohlEX6N17yZnVXxQFuh4fDRunP8ca6bfA==",
"optional": true
},
"@eslint-community/eslint-utils": {
@@ -24111,9 +24112,9 @@
"peer": true
},
"cypress": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.0.tgz",
"integrity": "sha512-UimjRSJJYdTlvkChcdcfywKJ6tUYuwYuk/n1uMMglrvi+ZthNhoRYcxnWgTqUtkl17fXrPAsD5XT2rcQYN1xKA==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
"integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.0",
@@ -25031,33 +25032,33 @@
}
},
"esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.1.tgz",
"integrity": "sha512-GPqx+FX7mdqulCeQ4TsGZQ3djBJkx5k7zBGtqt9ycVlWNg8llJ4RO9n2vciu8BN2zAEs6lPbPl0asZsAh7oWzg==",
"requires": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
"@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-wasm": {

View File

@@ -92,7 +92,7 @@
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.20.2",
"esbuild": "^0.21.1",
"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.7.0",
"cypress": "^13.9.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@@ -53,6 +53,44 @@ let routes: Routes = [
},
]
},
{
path: 'testnet4',
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
component: StatusViewComponent
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: '/testnet4'
},
]
},
{
path: 'signet',
children: [
@@ -130,6 +168,10 @@ let routes: Routes = [
path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet4',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
{
path: 'signet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)

View File

@@ -189,22 +189,22 @@ export const specialBlocks = {
'0': {
labelEvent: 'Genesis',
labelEventCompleted: 'The Genesis of Bitcoin',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'210000': {
labelEvent: 'Bitcoin\'s 1st Halving',
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'420000': {
labelEvent: 'Bitcoin\'s 2nd Halving',
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'630000': {
labelEvent: 'Bitcoin\'s 3rd Halving',
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'709632': {
labelEvent: 'Taproot 🌱 activation',
@@ -214,62 +214,62 @@ export const specialBlocks = {
'840000': {
labelEvent: 'Bitcoin\'s 4th Halving',
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1050000': {
labelEvent: 'Bitcoin\'s 5th Halving',
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1260000': {
labelEvent: 'Bitcoin\'s 6th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1470000': {
labelEvent: 'Bitcoin\'s 7th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1680000': {
labelEvent: 'Bitcoin\'s 8th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1890000': {
labelEvent: 'Bitcoin\'s 9th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2100000': {
labelEvent: 'Bitcoin\'s 10th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2310000': {
labelEvent: 'Bitcoin\'s 11th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2520000': {
labelEvent: 'Bitcoin\'s 12th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2730000': {
labelEvent: 'Bitcoin\'s 13th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2940000': {
labelEvent: 'Bitcoin\'s 14th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'3150000': {
labelEvent: 'Bitcoin\'s 15th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
}
};

View File

@@ -266,6 +266,11 @@ const featureActivation = {
segwit: 872730,
taproot: 2032291,
},
testnet4: {
rbf: 0,
segwit: 0,
taproot: 0,
},
signet: {
rbf: 0,
segwit: 0,

View File

@@ -21,9 +21,9 @@
<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;">Settlement expected in ~1 hour or less<br>
<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">sats</span></span>)
<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>
}
@@ -38,7 +38,13 @@
<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>
<span style="color: rgb(186, 186, 186); font-size: 14px;">Settlement expected to occur <app-time kind="within" [time]="eta" [fastRender]="false" [fixedRender]="true"></app-time></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>

View File

@@ -11,7 +11,7 @@ import { AudioService } from '../../services/audio.service';
styleUrls: ['./accelerate-checkout.component.scss']
})
export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() eta: number = Date.now() + 123456789;
@Input() eta: number | null = null;
@Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad';
@Input() scrollEvent: boolean;
@Output() close = new EventEmitter<null>();

View File

@@ -10,7 +10,7 @@
width: 100%;
height: 100%;
position: relative;
background: #181b2d;
background: var(--stat-box-bg);
.bar {
position: absolute;

View File

@@ -38,7 +38,7 @@
<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-accelerator">
<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>
@@ -87,7 +87,7 @@
<h5>Acceleration summary</h5>
<div class="row mb-3">
<div class="col">
<table class="table table-borderless table-border table-dark table-accelerator">
<table class="table table-borderless table-border table-dark table-background table-accelerator">
<tbody>
<!-- ESTIMATED FEE -->
<ng-container>

View File

@@ -110,3 +110,7 @@
.item {
white-space: initial;
}
.table-background {
background-color: var(--bg);
}

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
@@ -11,7 +11,7 @@ import { ServicesApiServices } from '../../../services/services-api.service';
styleUrls: ['./accelerations-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationsListComponent implements OnInit {
export class AccelerationsListComponent implements OnInit, OnDestroy {
@Input() widget: boolean = false;
@Input() pending: boolean = false;
@Input() accelerations$: Observable<Acceleration[]>;
@@ -44,7 +44,10 @@ export class AccelerationsListComponent implements OnInit {
this.accelerationList$ = this.pageSubject.pipe(
switchMap((page) => {
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page }));
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.stateService.liveAccelerations$ : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page }));
if (!this.accelerations$ && this.pending) {
this.websocketService.ensureTrackAccelerations();
}
return accelerationObservable$.pipe(
switchMap(response => {
let accelerations = response;
@@ -85,4 +88,8 @@ export class AccelerationsListComponent implements OnInit {
trackByBlock(index: number, block: BlockExtended): number {
return block.height;
}
ngOnDestroy(): void {
this.websocketService.stopTrackAccelerations();
}
}

View File

@@ -60,7 +60,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { Observable, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs';
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
import { Color } from '../../block-overview-graph/sprite-types';
import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view';
@@ -28,7 +28,7 @@ interface AccelerationBlock extends BlockExtended {
styleUrls: ['./accelerator-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AcceleratorDashboardComponent implements OnInit {
export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
blocks$: Observable<AccelerationBlock[]>;
accelerations$: Observable<Acceleration[]>;
pendingAccelerations$: Observable<Acceleration[]>;
@@ -39,6 +39,8 @@ export class AcceleratorDashboardComponent implements OnInit {
firstLoad = true;
timespan: '3d' | '1w' | '1m' = '1w';
accelerationDeltaSubscription: Subscription;
graphHeight: number = 300;
theme: ThemeService;
@@ -59,27 +61,28 @@ export class AcceleratorDashboardComponent implements OnInit {
ngOnInit(): void {
this.onResize();
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
this.websocketService.startTrackAccelerations();
this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).pipe(
startWith(true),
switchMap(() => {
return this.serviceApiServices.getAccelerations$().pipe(
catchError(() => {
return of([]);
}),
);
}),
tap(accelerations => {
if (!this.firstLoad && accelerations.some(acc => !this.seen.has(acc.txid))) {
this.audioService.playSound('bright-harmony');
}
for(const acc of accelerations) {
this.seen.add(acc.txid);
}
this.firstLoad = false;
}),
this.pendingAccelerations$ = this.stateService.liveAccelerations$.pipe(
share(),
);
this.accelerationDeltaSubscription = this.stateService.accelerations$.subscribe((delta) => {
if (!delta.reset) {
let hasNewAcceleration = false;
for (const acc of delta.added) {
if (!this.seen.has(acc.txid)) {
hasNewAcceleration = true;
}
this.seen.add(acc.txid);
}
for (const txid of delta.removed) {
this.seen.delete(txid);
}
if (hasNewAcceleration) {
this.audioService.playSound('bright-harmony');
}
}
});
this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
@@ -145,7 +148,7 @@ export class AcceleratorDashboardComponent implements OnInit {
} else {
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
return this.theme.theme === 'contrast' ? contrastColors[feeLevelIndex] || contrastColors[contrastColors.length - 1] : normalColors[feeLevelIndex] || normalColors[normalColors.length - 1];
return this.theme.theme === 'contrast' || this.theme.theme === 'bukele' ? contrastColors[feeLevelIndex] || contrastColors[contrastColors.length - 1] : normalColors[feeLevelIndex] || normalColors[normalColors.length - 1];
}
}
@@ -154,6 +157,11 @@ export class AcceleratorDashboardComponent implements OnInit {
return false;
}
ngOnDestroy(): void {
this.accelerationDeltaSubscription.unsubscribe();
this.websocketService.stopTrackAccelerations();
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {

View File

@@ -2,7 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
@Component({
selector: 'app-pending-stats',
@@ -15,11 +16,12 @@ export class PendingStatsComponent implements OnInit {
public accelerationStats$: Observable<any>;
constructor(
private servicesApiService: ServicesApiServices,
private stateService: StateService,
private websocketService: WebsocketService,
) { }
ngOnInit(): void {
this.accelerationStats$ = (this.accelerations$ || this.servicesApiService.getAccelerations$()).pipe(
this.accelerationStats$ = (this.accelerations$ || this.stateService.liveAccelerations$).pipe(
switchMap(accelerations => {
let totalAccelerations = 0;
let totalFeeDelta = 0;

View File

@@ -1,14 +1,8 @@
<app-indexing-progress></app-indexing-progress>
<div class="full-container">
<div class="card-header mb-0 mb-md-2">
<div class="d-flex d-md-block align-items-baseline">
<span i18n="address.balance-history">Balance History</span>
</div>
</div>
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div [class.full-container]="!widget">
<ng-container *ngIf="!error">
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
@@ -20,4 +14,8 @@
<p class="error">{{ error }}</p>
</div>
</ng-container>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
@@ -45,28 +46,12 @@
display: flex;
flex: 1;
width: 100%;
padding-bottom: 20px;
padding-bottom: 10px;
padding-right: 10px;
@media (max-width: 992px) {
padding-bottom: 25px;
}
@media (max-width: 829px) {
padding-bottom: 50px;
}
@media (max-width: 767px) {
padding-bottom: 25px;
}
@media (max-width: 629px) {
padding-bottom: 55px;
}
@media (max-width: 567px) {
padding-bottom: 55px;
}
}
.chart-widget {
width: 100%;
height: 100%;
max-height: 270px;
}
.disabled {

View File

@@ -1,12 +1,22 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { of } from 'rxjs';
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ChainStats } from '../../interfaces/electrs.interface';
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';
const periodSeconds = {
'1d': (60 * 60 * 24),
'3d': (60 * 60 * 24 * 3),
'1w': (60 * 60 * 24 * 7),
'1m': (60 * 60 * 24 * 30),
'6m': (60 * 60 * 24 * 180),
'1y': (60 * 60 * 24 * 365),
};
@Component({
selector: 'app-address-graph',
@@ -22,16 +32,23 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressGraphComponent implements OnChanges {
export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() address: string;
@Input() isPubkey: boolean = false;
@Input() stats: ChainStats;
@Input() addressSummary$: Observable<AddressTxSummary[]> | null;
@Input() period: '1d' | '3d' | '1w' | '1m' | '6m' | '1y' | 'all' = 'all';
@Input() height: number = 200;
@Input() right: number | string = 10;
@Input() left: number | string = 70;
@Input() widget: boolean = false;
data: any[] = [];
hoverData: any[] = [];
subscription: Subscription;
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
@@ -43,6 +60,7 @@ export class AddressGraphComponent implements OnChanges {
constructor(
@Inject(LOCALE_ID) public locale: string,
public stateService: StateService,
private electrsApiService: ElectrsApiService,
private router: Router,
private amountShortenerPipe: AmountShortenerPipe,
@@ -52,32 +70,59 @@ export class AddressGraphComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
(this.isPubkey
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
catchError(e => {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
).subscribe(addressSummary => {
if (addressSummary) {
this.error = null;
this.prepareChartOptions(addressSummary);
if (!this.address || !this.stats) {
return;
}
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
if (this.subscription) {
this.subscription.unsubscribe();
}
this.isLoading = false;
this.cd.markForCheck();
});
this.subscription = combineLatest([
this.redraw$,
(this.addressSummary$ || (this.isPubkey
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
catchError(e => {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
))
]).subscribe(([redraw, addressSummary]) => {
if (addressSummary) {
this.error = null;
this.prepareChartOptions(addressSummary);
}
this.isLoading = false;
this.cd.markForCheck();
});
} else {
// re-trigger subscription
this.redraw$.next(true);
}
}
prepareChartOptions(summary): void {
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); // + (summary[0]?.value || 0);
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];
}).reverse();
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1])), 0);
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 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);
this.chartOptions = {
color: [
@@ -108,6 +153,9 @@ export class AddressGraphComponent implements OnChanges {
},
borderColor: '#000',
formatter: function (data): string {
if (!data?.length || !data[0]?.data?.[2]?.txid) {
return '';
}
const header = data.length === 1
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
: `${data.length} transactions`;
@@ -141,13 +189,17 @@ export class AddressGraphComponent implements OnChanges {
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val): string => {
if (maxValue > 1_000_000_000) {
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
if (valSpan > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`;
} else if (maxValue > 100_000_000) {
}
else if (valSpan > 1_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`;
} else if (valSpan > 100_000_000) {
return `${(val / 100_000_000).toFixed(1)} BTC`;
} else if (maxValue > 10_000_000) {
} else if (valSpan > 10_000_000) {
return `${(val / 100_000_000).toFixed(2)} BTC`;
} else if (maxValue > 1_000_000) {
} else if (valSpan > 1_000_000) {
return `${(val / 100_000_000).toFixed(3)} BTC`;
} else {
return `${this.amountShortenerPipe.transform(val, 0)} sats`;
@@ -157,6 +209,7 @@ export class AddressGraphComponent implements OnChanges {
splitLine: {
show: false,
},
min: this.period === 'all' ? 0 : 'dataMin'
},
],
series: [
@@ -194,6 +247,12 @@ export class AddressGraphComponent implements OnChanges {
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
isMobile() {
return (window.innerWidth <= 767.98);
}

View File

@@ -1,6 +1,6 @@
.frame {
position: relative;
background: #24273e;
background: var(--box-bg);
padding: 0.5rem;
height: calc(100% + 60px);
}
@@ -62,7 +62,7 @@
}
}
&:nth-child(even) {
background: #181b2d;
background: var(--stat-box-bg);
}
}

View File

@@ -0,0 +1,32 @@
<table class="table latest-transactions">
<thead>
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
<th class="table-cell-fiat">{{ currency }}</th>
<th class="table-cell-date" i18n="shared.date">Date</th>
</thead>
<tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton">
<tr *ngFor="let transaction of transactions; let i = index;">
<td class="table-cell-txid">
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
</a>
</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>
</tr>
</tbody>
<div class="">&nbsp;</div>
</table>
<ng-template #recentTransactionsSkeleton>
<tbody>
<tr *ngFor="let i of [1,2,3,4,5,6]">
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
<td class="table-cell-satoshis"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-fiat"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-fees"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
</tr>
</tbody>
</ng-template>

View File

@@ -0,0 +1,50 @@
.latest-transactions {
width: 100%;
text-align: left;
table-layout:fixed;
tr, td, th {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.75rem !important;
}
td {
overflow:hidden;
width: 25%;
}
.table-cell-satoshis {
display: none;
text-align: right;
@media (min-width: 576px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 1100px) {
display: table-cell;
}
}
.table-cell-fiat {
display: none;
text-align: right;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: table-cell;
}
}
.table-cell-date {
text-align: right;
}
}
.skeleton-loader-transactions {
max-width: 250px;
position: relative;
top: 2px;
margin-bottom: -3px;
height: 18px;
}

View File

@@ -0,0 +1,76 @@
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
import { PriceService } from '../../services/price.service';
@Component({
selector: 'app-address-transactions-widget',
templateUrl: './address-transactions-widget.component.html',
styleUrls: ['./address-transactions-widget.component.scss'],
})
export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, OnDestroy {
@Input() address: string;
@Input() addressInfo: Address;
@Input() addressSummary$: Observable<AddressTxSummary[]> | null;
@Input() isPubkey: boolean = false;
currencySubscription: Subscription;
currency: string;
transactions$: Observable<any[]>;
isLoading: boolean = true;
error: any;
constructor(
public stateService: StateService,
private electrsApiService: ElectrsApiService,
private priceService: PriceService,
) { }
ngOnInit(): void {
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
this.currency = fiat;
});
this.startAddressSubscription();
}
ngOnChanges(changes: SimpleChanges): void {
this.startAddressSubscription();
}
startAddressSubscription(): void {
this.isLoading = true;
if (!this.address || !this.addressInfo) {
return;
}
this.transactions$ = (this.addressSummary$ || (this.isPubkey
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
catchError(e => {
this.error = `Failed to fetch address history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
})
)).pipe(
map(summary => {
return summary?.slice(0, 6);
}),
switchMap(txs => {
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, true, this.currency).pipe(
map(price => {
return {
...tx,
price,
};
})
))));
})
);
}
ngOnDestroy(): void {
this.currencySubscription.unsubscribe();
}
}

View File

@@ -3,7 +3,7 @@
}
.qr-wrapper {
background-color: var(--fg);
background-color: #fff;
padding: 10px;
padding-bottom: 5px;
display: inline-block;

View File

@@ -53,10 +53,20 @@
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2">
<br>
<div class="title-tx">
<h2 class="text-left" i18n="address.balance-history">Balance History</h2>
</div>
<div class="box">
<div class="widget-toggler" *ngIf="showBalancePeriod()">
<a href="" (click)="setBalancePeriod('all')" class="toggler-option"
[ngClass]="{'inactive': balancePeriod === 'all'}"><small i18n="all">all</small></a>
<span style="color: var(--transparent-fg); font-size: 8px"> | </span>
<a href="" (click)="setBalancePeriod('1m')" class="toggler-option"
[ngClass]="{'inactive': balancePeriod === '1m'}"><small i18n="recent">recent</small></a>
</div>
<div class="row">
<div class="col-md">
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" />
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" [period]="balancePeriod" />
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
.qr-wrapper {
background-color: var(--fg);
background-color: #fff;
padding: 10px;
padding-bottom: 5px;
display: inline-block;
@@ -109,3 +109,19 @@ h1 {
flex-grow: 0.5;
}
}
.widget-toggler {
font-size: 12px;
position: absolute;
top: -20px;
right: 3px;
text-align: right;
}
.toggler-option {
text-decoration: none;
}
.inactive {
color: var(--transparent-fg);
}

View File

@@ -38,6 +38,8 @@ export class AddressComponent implements OnInit, OnDestroy {
txCount = 0;
received = 0;
sent = 0;
now = Date.now() / 1000;
balancePeriod: 'all' | '1m' = 'all';
private tempTransactions: Transaction[];
private timeTxIndexes: number[];
@@ -174,6 +176,10 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions = this.tempTransactions;
this.isLoadingTransactions = false;
if (!this.showBalancePeriod()) {
this.setBalancePeriod('all');
}
},
(error) => {
console.log(error);
@@ -296,6 +302,18 @@ export class AddressComponent implements OnInit, OnDestroy {
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
}
setBalancePeriod(period: 'all' | '1m'): boolean {
this.balancePeriod = period;
return false;
}
showBalancePeriod(): boolean {
return this.transactions?.length && (
!this.transactions[0].status?.confirmed
|| this.transactions[0].status.block_time > (this.now - (60 * 60 * 24 * 30))
);
}
ngOnDestroy() {
this.mainSubscription.unsubscribe();
this.mempoolTxSubscription.unsubscribe();

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
<ng-container *ngIf="!noFiat && (viewAmountMode$ | async) === 'fiat' && (conversions$ | async) as conversions; else viewFiatVin">
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
{{ addPlus && satoshis >= 0 ? '+' : '' }}{{
(
@@ -20,10 +20,29 @@
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
<span i18n="shared.confidential">Confidential</span>
</ng-template>
<ng-template #default>&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
<span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC</span>
<ng-template #default>
@if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat') {
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
<span class="symbol">
<ng-container *ngTemplateOutlet="prefix"></ng-container>BTC
</span>
} @else {
@if (digitsInfo === '1.8-8') {
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
} @else {
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : satoshis < 1000 && satoshis > -1000 ? 0 : 1 }}
}
<span class="symbol">
<ng-container *ngTemplateOutlet="prefix"></ng-container>sats
</span>
}
</ng-template>
</ng-template>
<ng-template #prefix>
<ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
<ng-template [ngIf]="network === 'testnet4'">t</ng-template>
<ng-template [ngIf]="network === 'signet'">s</ng-template>
</ng-template>

View File

@@ -12,7 +12,7 @@ import { Price } from '../../services/price.service';
export class AmountComponent implements OnInit, OnDestroy {
conversions$: Observable<any>;
currency: string;
viewFiat$: Observable<boolean>;
viewAmountMode$: Observable<'btc' | 'sats' | 'fiat'>;
network = '';
stateSubscription: Subscription;
@@ -37,7 +37,7 @@ export class AmountComponent implements OnInit, OnDestroy {
}
ngOnInit() {
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
this.viewAmountMode$ = this.stateService.viewAmountMode$.asObservable();
this.conversions$ = this.stateService.conversions$.asObservable();
this.stateSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
}

View File

@@ -1,5 +1,5 @@
.qr-wrapper {
background-color: var(--fg);
background-color: #fff;
padding: 10px;
padding-bottom: 5px;
display: inline-block;

View File

@@ -18,7 +18,7 @@
<br>
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="ellipses"></ngb-pagination>
<ngb-pagination class="pagination-container" [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="ellipses"></ngb-pagination>
<div class="clearfix"></div>
<br>

View File

@@ -0,0 +1,59 @@
<div class="card">
<div class="card-body more-padding">
<div class="balance-container" *ngIf="!isLoading; else loading">
<div class="item">
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
<div class="card-text">
{{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
</div>
<div class="symbol">
<app-fiat [value]="(addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum)"></app-fiat>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="dashboard.7d-change">Change (7d)</h5>
<div class="card-text">
{{ delta7d > 0 ? '+' : ''}}{{ ((delta7d) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
</div>
<div class="symbol">
<app-fiat [value]="delta7d"></app-fiat>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="dashboard.30d-change">Change (30d)</h5>
<div class="card-text">
{{ delta30d > 0 ? '+' : ''}}{{ ((delta30d) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
</div>
<div class="symbol">
<app-fiat [value]="delta30d"></app-fiat>
</div>
</div>
</div>
</div>
</div>
<ng-template #loading>
<div class="balance-skeleton">
<div class="item">
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="dashboard.7d-change">Change (7d)</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="dashboard.30d-change">Change (30d)</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,160 @@
.balance-container {
display: flex;
flex-direction: row;
justify-content: space-around;
height: 76px;
.shared-block {
color: var(--transparent-fg);
font-size: 12px;
}
.item {
padding: 0 5px;
width: 100%;
max-width: 150px;
&:last-child {
display: none;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: table-cell;
}
}
}
.card-text {
font-size: 22px;
margin-top: -9px;
position: relative;
}
}
.balance-skeleton {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
min-width: 120px;
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
&:last-child{
display: none;
@media (min-width: 485px) {
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
&:last-child {
margin-bottom: 0;
}
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
margin: 14px auto 0;
max-width: 80px;
}
&:last-child {
margin: 10px auto 0;
max-width: 120px;
}
}
}
}
.card {
background-color: var(--bg);
height: 126px;
}
.card-title {
color: var(--title-fg);
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progress {
display: inline-flex;
width: 100%;
background-color: var(--secondary);
height: 1.1rem;
max-width: 180px;
}
.skeleton-loader {
max-width: 100%;
}
.more-padding {
padding: 24px 20px;
}
.small-bar {
height: 8px;
top: -4px;
max-width: 120px;
}
.loading-container {
min-height: 76px;
}
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.card-wrapper {
.card {
height: auto !important;
}
.card-body {
display: flex;
flex: inherit;
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 24px 20px;
}
}
.retarget-sign {
margin-right: -3px;
font-size: 14px;
top: -2px;
position: relative;
}
.previous-retarget-sign {
margin-right: -2px;
font-size: 10px;
}
.symbol {
font-size: 13px;
white-space: nowrap;
}

View File

@@ -0,0 +1,71 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { Observable, catchError, of } from 'rxjs';
@Component({
selector: 'app-balance-widget',
templateUrl: './balance-widget.component.html',
styleUrls: ['./balance-widget.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BalanceWidgetComponent implements OnInit, OnChanges {
@Input() address: string;
@Input() addressInfo: Address;
@Input() addressSummary$: Observable<AddressTxSummary[]> | null;
@Input() isPubkey: boolean = false;
isLoading: boolean = true;
error: any;
delta7d: number = 0;
delta30d: number = 0;
constructor(
public stateService: StateService,
private electrsApiService: ElectrsApiService,
private cd: ChangeDetectorRef,
) { }
ngOnInit(): void {
}
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
if (!this.address || !this.addressInfo) {
return;
}
(this.addressSummary$ || (this.isPubkey
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
catchError(e => {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
)).subscribe(addressSummary => {
if (addressSummary) {
this.error = null;
this.calculateStats(addressSummary);
}
this.isLoading = false;
this.cd.markForCheck();
});
}
calculateStats(summary: AddressTxSummary[]): void {
let weekTotal = 0;
let monthTotal = 0;
const weekAgo = (Date.now() / 1000) - (60 * 60 * 24 * 7);
const monthAgo = (Date.now() / 1000) - (60 * 60 * 24 * 30);
for (let i = 0; i < summary.length && summary[i].time >= monthAgo; i++) {
monthTotal += summary[i].value;
if (summary[i].time >= weekAgo) {
weekTotal += summary[i].value;
}
}
this.delta7d = weekTotal;
this.delta30d = monthTotal;
}
}

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -136,7 +136,7 @@
:host-context(.block-overview-graph:hover) &, &:hover, &:active {
.menu-toggle {
opacity: 0.5;
background: #181b2d;
background: var(--stat-box-bg);
&:hover {
opacity: 1;

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -81,6 +81,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
tooltipPosition: Position;
readyNextFrame = false;
lastUpdate: number = 0;
pendingUpdate: {
count: number,
add: { [txid: string]: TransactionStripped },
remove: { [txid: string]: string },
change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } },
direction?: string,
} = {
count: 0,
add: {},
remove: {},
change: {},
direction: 'left',
};
searchText: string;
searchSubscription: Subscription;
@@ -176,6 +190,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
destroy(): void {
if (this.scene) {
this.scene.destroy();
this.clearUpdateQueue();
this.start();
}
}
@@ -188,6 +203,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
this.filtersAvailable = filtersAvailable;
if (this.scene) {
this.clearUpdateQueue();
this.scene.setup(transactions);
this.readyNextFrame = true;
this.start();
@@ -197,6 +213,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
enter(transactions: TransactionStripped[], direction: string): void {
if (this.scene) {
this.clearUpdateQueue();
this.scene.enter(transactions, direction);
this.start();
this.updateSearchHighlight();
@@ -205,6 +222,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
exit(direction: string): void {
if (this.scene) {
this.clearUpdateQueue();
this.scene.exit(direction);
this.start();
this.updateSearchHighlight();
@@ -213,13 +231,67 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
if (this.scene) {
this.clearUpdateQueue();
this.scene.replace(transactions || [], direction, sort, startTime);
this.start();
this.updateSearchHighlight();
}
}
// collates deferred updates into a set of consistent pending changes
queueUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
for (const tx of add) {
this.pendingUpdate.add[tx.txid] = tx;
delete this.pendingUpdate.remove[tx.txid];
delete this.pendingUpdate.change[tx.txid];
}
for (const txid of remove) {
delete this.pendingUpdate.add[txid];
this.pendingUpdate.remove[txid] = txid;
delete this.pendingUpdate.change[txid];
}
for (const tx of change) {
if (this.pendingUpdate.add[tx.txid]) {
this.pendingUpdate.add[tx.txid].rate = tx.rate;
this.pendingUpdate.add[tx.txid].acc = tx.acc;
} else {
this.pendingUpdate.change[tx.txid] = tx;
}
}
this.pendingUpdate.direction = direction;
this.pendingUpdate.count++;
}
deferredUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
this.queueUpdate(add, remove, change, direction);
this.applyQueuedUpdates();
}
applyQueuedUpdates(): void {
if (this.pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), this.pendingUpdate.direction);
this.clearUpdateQueue();
}
}
clearUpdateQueue(): void {
this.pendingUpdate = {
count: 0,
add: {},
remove: {},
change: {},
};
this.lastUpdate = performance.now();
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
// merge any pending changes into this update
this.queueUpdate(add, remove, change);
this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), direction, resetLayout);
this.clearUpdateQueue();
}
applyUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) {
add = add.filter(tx => !this.scene.txs[tx.txid]);
remove = remove.filter(txid => this.scene.txs[txid]);
@@ -230,6 +302,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
this.scene.update(add, remove, change, direction, resetLayout);
this.start();
this.lastUpdate = performance.now();
this.updateSearchHighlight();
}
}
@@ -370,6 +443,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (!now) {
now = performance.now();
}
this.applyQueuedUpdates();
// skip re-render if there's no change to the scene
if (this.scene && this.gl) {
/* SET UP SHADER UNIFORMS */
@@ -577,13 +651,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
return (tx: TxView) => {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
if (this.themeService.theme !== 'contrast') {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
} else {
return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
}
} else {
if (this.themeService.theme !== 'contrast') {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
tx,
defaultColors.unmatchedfee,

View File

@@ -13,7 +13,7 @@ export default class BlockScene {
theme: ThemeService;
orientation: string;
flip: boolean;
animationDuration: number = 900;
animationDuration: number = 1000;
configAnimationOffset: number | null;
animationOffset: number;
highlightingEnabled: boolean;
@@ -69,7 +69,7 @@ export default class BlockScene {
}
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
this.theme.theme === 'contrast' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
this.theme.theme === 'contrast' || this.theme.theme === 'bukele' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
this.updateAllColors();
}
@@ -179,7 +179,7 @@ export default class BlockScene {
removed.forEach(tx => {
tx.destroy();
});
}, 1000);
}, (startTime - performance.now()) + this.animationDuration + 1000);
if (resetLayout) {
add.forEach(tx => {
@@ -239,14 +239,14 @@ export default class BlockScene {
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
): void {
this.animationDuration = animationDuration || 1000;
this.animationDuration = animationDuration || this.animationDuration || 1000;
this.configAnimationOffset = animationOffset;
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.orientation = orientation;
this.flip = flip;
this.vertexArray = vertexArray;
this.highlightingEnabled = highlighting;
theme.theme === 'contrast' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
theme.theme === 'contrast' || theme.theme === 'bukele' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
this.theme = theme;
this.scene = {

View File

@@ -177,7 +177,7 @@ export function ageColorFunction(
return auditColors.accelerated;
}
const color = theme !== 'contrast' ? defaultColorFunction(tx, colors, auditColors, relativeTime) : contrastColorFunction(tx, colors, auditColors, relativeTime);
const color = theme !== 'contrast' && theme !== 'bukele' ? defaultColorFunction(tx, colors, auditColors, relativeTime) : contrastColorFunction(tx, colors, auditColors, relativeTime);
const ageLevel = (!tx.time ? 0 : (0.8 * Math.tanh((1 / 15) * Math.log2((Math.max(1, 0.6 * ((relativeTime - tx.time) - 60)))))));
return {

View File

@@ -3,7 +3,7 @@
class="block-overview-tooltip"
[class.clickable]="clickable"
[style.visibility]="tx ? 'visible' : 'hidden'"
[style.left]="tooltipPosition.x + 'px'"
[style.left]="getTooltipLeftPosition()"
[style.top]="tooltipPosition.y + 'px'"
>
<table class="table-fixed">

View File

@@ -1,6 +1,6 @@
.block-overview-tooltip {
position: absolute;
background: rgba(#11131f, 0.95);
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: var(--tooltip-grey);
@@ -10,7 +10,7 @@
padding: 10px 15px;
text-align: left;
min-width: 340px;
max-width: 340px;
max-width: 400px;
pointer-events: none;
z-index: 11;
@@ -30,7 +30,7 @@ th, td {
}
.badge.badge-accelerated {
background-color: var(--tertiary);
background-color: #653b9c;
box-shadow: #ad7de57f 0px 0px 12px -2px;
color: white;
animation: acceleratePulse 1s infinite;
@@ -41,7 +41,7 @@ th, td {
flex-wrap: wrap;
row-gap: 0.25em;
margin-top: 0.2em;
max-width: 100%;
max-width: 310px;
.badge {
border-radius: 0.2rem;
@@ -71,7 +71,7 @@ th, td {
}
@keyframes acceleratePulse {
0% { background-color: var(--tertiary); box-shadow: #ad7de57f 0px 0px 12px -2px; }
0% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
50% { background-color: #8457bb; box-shadow: #ad7de5 0px 0px 18px -2px;}
100% { background-color: var(--tertiary); box-shadow: #ad7de57f 0px 0px 12px -2px; }
100% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
}

View File

@@ -96,4 +96,8 @@ export class BlockOverviewTooltipComponent implements OnChanges {
this.cd.markForCheck();
}
}
getTooltipLeftPosition(): string {
return window.innerWidth < 392 ? '-50px' : this.tooltipPosition.x + 'px';
}
}

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -1,7 +1,7 @@
.block-wrapper {
width: 100vw;
height: 100vh;
background: #181b2d;
background: var(--stat-box-bg);
}
.block-container {

View File

@@ -12,6 +12,7 @@
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
[class.offscreen]="!static && count && i >= count"
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
[style]="blockTransformation"
[class.blink-bg]="isSpecial(block.height)">
<a draggable="false" [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
@@ -40,7 +41,7 @@
<app-fee-rate unitClass=""></app-fee-rate>
</div>
</ng-template>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo$ | async; else noMiningInfo"
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="blockDisplayMode === 'fees'; else noMiningInfo"
class="block-size">
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div>

View File

@@ -63,7 +63,7 @@
.fee-span {
font-size: 11px;
margin-bottom: 5px;
color: #fff000;
color: var(--yellow);
}
.transaction-count {
@@ -130,7 +130,7 @@
height: 0;
border-left: calc(var(--block-size) * 0.3) solid transparent;
border-right: calc(var(--block-size) * 0.3) solid transparent;
border-bottom: calc(var(--block-size) * 0.3) solid #FFF;
border-bottom: calc(var(--block-size) * 0.3) solid var(--fg);
}
.flashing {

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { Observable, Subscription, delay, filter, tap } from 'rxjs';
import { StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
import { BlockExtended } from '../../interfaces/node-api.interface';
@@ -45,7 +45,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
markBlockSubscription: Subscription;
txConfirmedSubscription: Subscription;
loadingBlocks$: Observable<boolean>;
showMiningInfo$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
showMiningInfoSubscription: Subscription;
blockDisplayModeSubscription: Subscription;
blockDisplayMode: 'size' | 'fees';
blockTransformation = {};
blockStyles = [];
emptyBlockStyles = [];
interval: any;
@@ -67,6 +70,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
liquid: ['var(--liquid)', 'var(--testnet-alt)'],
'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'],
testnet: ['var(--testnet)', 'var(--testnet-alt)'],
testnet4: ['var(--testnet)', 'var(--testnet-alt)'],
signet: ['var(--signet)', 'var(--signet-alt)'],
};
@@ -78,22 +82,38 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
) {
}
enabledMiningInfoIfNeeded(url) {
const urlParts = url.split('/');
const onDashboard = ['','testnet','signet','mining','acceleration'].includes(urlParts[urlParts.length - 1]);
if (onDashboard) { // Only update showMiningInfo if we are on the main, mining or acceleration dashboards
this.stateService.showMiningInfo$.next(url.includes('/mining') || url.includes('/acceleration'));
}
}
ngOnInit() {
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.enabledMiningInfoIfNeeded(this.location.path());
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
this.showMiningInfo$ = this.stateService.showMiningInfo$;
}
this.blockDisplayMode = this.stateService.blockDisplayMode$.value as 'size' | 'fees';
this.blockDisplayModeSubscription = this.stateService.blockDisplayMode$
.pipe(
filter((mode: 'size' | 'fees') => mode !== this.blockDisplayMode),
tap(() => {
this.blockTransformation = this.timeLtr ? {
transform: 'scaleX(-1) rotateX(90deg)',
transition: 'transform 0.375s'
} : {
transform: 'rotateX(90deg)',
transition: 'transform 0.375s'
};
}),
delay(375),
tap((mode) => {
this.blockDisplayMode = mode;
this.blockTransformation = this.timeLtr ? {
transform: 'scaleX(-1)',
transition: 'transform 0.375s'
} : {
transition: 'transform 0.375s'
};
this.cd.markForCheck();
}),
delay(375),
)
.subscribe(() => {
this.blockTransformation = {};
});
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
this.timeLtr = !!ltr;
@@ -204,6 +224,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.networkSubscription.unsubscribe();
this.tabHiddenSubscription.unsubscribe();
this.markBlockSubscription.unsubscribe();
this.blockDisplayModeSubscription.unsubscribe();
this.timeLtrSubscription.unsubscribe();
clearInterval(this.interval);
}
@@ -329,7 +350,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
left: addLeft + this.blockOffset * index + 'px',
background: `repeating-linear-gradient(
#2d3348,
var(--secondary),
var(--secondary) ${greenBackgroundHeight}%,
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,
${this.gradientColors[this.network][1]} 100%
@@ -341,7 +362,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
convertStyleForLoadingBlock(style) {
return {
...style,
background: "#2d3348",
background: "var(--secondary)",
};
}
@@ -350,7 +371,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
left: addLeft + (this.blockOffset * index) + 'px',
background: "#2d3348",
background: "var(--secondary)",
};
}

View File

@@ -10,6 +10,7 @@
</ng-container>
</div>
<div id="divider" [hidden]="pageIndex > 0">
<button class="block-display-toggle" (click)="toggleBlockDisplayMode()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
<button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
</div>
</span>

View File

@@ -54,7 +54,7 @@
}
.time-toggle {
color: white;
color: var(--fg);
font-size: 0.8rem;
position: absolute;
bottom: -1.8em;
@@ -67,9 +67,24 @@
padding: 0;
}
.block-display-toggle {
color: var(--fg);
font-size: 0.8rem;
position: absolute;
bottom: 15.8em;
left: 1px;
transform: translateX(-50%) rotate(90deg);
background: none;
border: none;
outline: none;
margin: 0;
padding: 0;
}
.blockchain-wrapper.ltr-transition .blocks-wrapper,
.blockchain-wrapper.ltr-transition .position-container,
.blockchain-wrapper.ltr-transition .time-toggle {
.blockchain-wrapper.ltr-transition .time-toggle,
.blockchain-wrapper.ltr-transition .block-display-toggle {
transition: transform 1s;
}
@@ -81,6 +96,10 @@
.time-toggle {
transform: translateX(-50%) scaleX(-1);
}
.block-display-toggle {
transform: translateX(-50%) scaleX(-1) rotate(90deg);
}
}
:host-context(.ltr-layout) {

View File

@@ -1,6 +1,7 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
import { firstValueFrom, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { StorageService } from '../../services/storage.service';
@Component({
selector: 'app-blockchain',
@@ -26,15 +27,18 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
connectionStateSubscription: Subscription;
loadingTip: boolean = true;
connected: boolean = true;
blockDisplayMode: 'size' | 'fees';
dividerOffset: number | null = null;
mempoolOffset: number | null = null;
positionStyle = {
transform: "translateX(1280px)",
};
blockDisplayToggleStyle = {};
constructor(
public stateService: StateService,
public StorageService: StorageService,
private cd: ChangeDetectorRef,
) {}
@@ -51,6 +55,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
firstValueFrom(this.stateService.chainTip$).then(() => {
this.loadingTip = false;
});
this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'fees';
}
ngOnDestroy(): void {
@@ -84,6 +89,13 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
}, 0);
}
toggleBlockDisplayMode(): void {
if (this.blockDisplayMode === 'size') this.blockDisplayMode = 'fees';
else this.blockDisplayMode = 'size';
this.StorageService.setValue('block-display-mode-preference', this.blockDisplayMode);
this.stateService.blockDisplayMode$.next(this.blockDisplayMode);
}
onMempoolWidthChange(width): void {
if (this.flipping) {
return;

View File

@@ -32,11 +32,12 @@ export class ClockComponent implements OnInit {
limitHeight: number;
gradientColors = {
'': ['#9339f4', '#105fb0'],
liquid: ['#116761', '#183550'],
'liquidtestnet': ['#494a4a', '#272e46'],
testnet: ['#1d486f', '#183550'],
signet: ['#6f1d5d', '#471850'],
'': ['var(--mainnet-alt)', 'var(--primary)'],
liquid: ['var(--liquid)', 'var(--testnet-alt)'],
'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'],
testnet: ['var(--testnet)', 'var(--testnet-alt)'],
testnet4: ['var(--testnet)', 'var(--testnet-alt)'],
signet: ['var(--signet)', 'var(--signet-alt)'],
};
constructor(
@@ -99,8 +100,8 @@ export class ClockComponent implements OnInit {
return {
background: `repeating-linear-gradient(
#2d3348,
#2d3348 ${greenBackgroundHeight}%,
var(--secondary),
var(--secondary) ${greenBackgroundHeight}%,
${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%,
${this.gradientColors[''][1]} 100%
)`,

View File

@@ -0,0 +1,286 @@
<div class="container-xl dashboard-container">
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
@for (widget of widgets; track widget.component) {
@switch (widget.component) {
@case ('fees') {
<div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card">
<div class="card-body less-padding">
<app-fees-box class="d-block"></app-fees-box>
</div>
</div>
</div>
}
@case ('difficulty') {
<div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
<app-difficulty></app-difficulty>
</div>
}
@case ('goggles') {
<div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
<h5 class="card-title d-inline"><span>Mempool Goggles&trade;</span> : {{ goggleCycle[goggleIndex].name }}</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<div class="quick-filter">
<div class="btn-group btn-group-toggle">
<label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle">
<input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }}
</label>
</div>
</div>
<div class="mempool-block-wrapper" *ngIf="webGlEnabled">
<app-mempool-block-overview
[index]="0"
[resolution]="goggleResolution"
[filterFlags]="goggleFlags"
[filterMode]="goggleMode"
[gradientMode]="gradientMode"
></app-mempool-block-overview>
</div>
</div>
</div>
</div>
}
@case ('incoming') {
<div class="col" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body">
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
<h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
<app-incoming-transactions-graph
[height]="incomingGraphHeight"
[left]="50"
[right]="20"
[data]="mempoolStats?.weightPerSecond"
[windowPreferenceOverride]="'2h'"
></app-incoming-transactions-graph>
</div>
</div>
</div>
</div>
<ng-template #mempoolTable let-mempoolInfoData>
<div class="mempool-info-data">
<div class="item">
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === mempoolInfoData.value.memPoolInfo.minrelaytxfee || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
<ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee !== mempoolInfoData.value.memPoolInfo.minrelaytxfee">&lt; </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</h5>
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
{{ mempoolInfoData.value.memPoolInfo.size | number }} <span i18n="dashboard.txs">TXs</span>
</p>
</div>
<div class="item bar">
<h5 class="card-title" i18n="dashboard.memory-usage|Memory usage">Memory Usage</h5>
<div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
<div class="progress">
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }">&nbsp;</div>
<div class="progress-text">&lrm;<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
</div>
</div>
</div>
</div>
</ng-template>
}
@case ('replacements') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<table class="table lastest-replacements-table">
<thead>
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
<th class="table-cell-old-fee" i18n="dashboard.previous-transaction-fee">Previous fee</th>
<th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
<th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
</thead>
<tbody *ngIf="replacements$ | async as replacements; else replacementsSkeleton">
<tr *ngFor="let replacement of replacements">
<td class="table-cell-txid">
<a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
<app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
</a>
</td>
<td class="table-cell-old-fee"><app-fee-rate [fee]="replacement.oldFee" [weight]="replacement.oldVsize * 4"></app-fee-rate></td>
<td class="table-cell-new-fee"><app-fee-rate [fee]="replacement.newFee" [weight]="replacement.newVsize * 4"></app-fee-rate></td>
<td class="table-cell-badges">
<span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
<span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
<span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="tx-features.tag.rbf|RBF">RBF</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<ng-template #replacementsSkeleton>
<tbody>
<tr *ngFor="let i of [1,2,3,4,5,6]">
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-old-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-new-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-badges"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
</tr>
</tbody>
</ng-template>
}
@case ('blocks') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<table class="table lastest-blocks-table">
<thead>
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
<th class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
</thead>
<tbody *ngIf="blocks$ | async as blocks; else blocksSkeleton">
<tr *ngFor="let block of blocks; let i = index; trackBy: trackByBlock">
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
<td class="table-cell-size">
<div class="progress">
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }">&nbsp;</div>
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<ng-template #blocksSkeleton>
<tbody>
<tr *ngFor="let i of [1,2,3,4,5,6]">
<td class="table-cell-height"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
<td class="table-cell-mined"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-transaction-count"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-size"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
</tr>
</tbody>
</ng-template>
}
@case ('transactions') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5>
<table class="table latest-transactions">
<thead>
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''">{{ currency }}</th>
<th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
</thead>
<tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton">
<tr *ngFor="let transaction of transactions; let i = index;">
<td class="table-cell-txid">
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
</a>
</td>
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-fees"><app-fee-rate [fee]="transaction.fee" [weight]="transaction.vsize * 4"></app-fee-rate></td>
</tr>
</tbody>
</table>
<div class="">&nbsp;</div>
</div>
</div>
</div>
<ng-template #recentTransactionsSkeleton>
<tbody>
<tr *ngFor="let i of [1,2,3,4,5,6]">
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
<td class="table-cell-satoshis"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
<td class="table-cell-fees"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
</tr>
</tbody>
</ng-template>
}
@case ('balance') {
<div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="main-title" i18n="dashboard.treasury">Treasury</div>
<app-balance-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-balance-widget>
</div>
}
@case ('address') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.balance-history">Balance History</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [height]="graphHeight"></app-address-graph>
</div>
</div>
</div>
}
@case ('addressTransactions') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-address-transactions-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-address-transactions-widget>
</div>
</div>
</div>
}
@case ('twitter') {
<div class="col" style="min-height:410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2 d-flex flex-column">
<a class="title-link" [href]="'https://x.com/' + widget.props?.handle" target="_blank">
<h5 class="card-title d-inline" i18n="dashboard.x-timeline">X Timeline</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
@defer {
<app-twitter-widget [handle]="widget.props?.handle" style="flex-grow: 1"></app-twitter-widget>
}
</div>
</div>
</div>
}
}
}
</div>
</div>
<ng-template #loading>
<div class="skeleton-loader"></div>
</ng-template>
<ng-template #loadingbig>
<span class="skeleton-loader skeleton-loader-big" ></span>
</ng-template>

View File

@@ -0,0 +1,490 @@
.dashboard-container {
text-align: center;
margin-top: 0.5rem;
.col {
margin-bottom: 1.5rem;
}
}
.card {
background-color: var(--bg);
height: 100%;
}
.card-title {
color: var(--title-fg);
font-size: 1rem;
}
.info-block {
float: left;
width: 350px;
line-height: 25px;
}
.progress {
display: inline-flex;
width: 100%;
background-color: var(--secondary);
height: 1.1rem;
max-width: 180px;
}
.bg-warning {
background-color: #b58800 !important;
}
.skeleton-loader {
max-width: 100%;
}
.more-padding {
padding: 18px;
}
.graph-card {
height: 100%;
@media (min-width: 768px) {
height: 415px;
}
@media (min-width: 992px) {
height: 510px;
}
}
.mempool-info-data {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
&.lbtc-pegs-stats {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
margin: 0px auto 20px;
display: inline-block;
@media (min-width: 485px) {
margin: 0px auto 10px;
}
@media (min-width: 768px) {
margin: 0px auto 0px;
}
&:last-child {
margin: 0px auto 0px;
}
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-text {
font-size: 18px;
span {
color: var(--transparent-fg);
font-size: 12px;
}
.bitcoin-color {
color: var(--orange);
}
}
.progress {
width: 90%;
@media (min-width: 768px) {
width: 100%;
}
}
}
.bar {
width: 93%;
margin: 0px 5px 20px;
@media (min-width: 485px) {
max-width: 200px;
margin: 0px auto 0px;
}
}
.skeleton-loader {
width: 100%;
max-width: 100px;
display: block;
margin: 18px auto 0;
}
.skeleton-loader-big {
max-width: 180px;
}
}
.latest-transactions {
width: 100%;
text-align: left;
table-layout:fixed;
tr, td, th {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.75rem !important;
}
td {
overflow:hidden;
width: 25%;
}
.table-cell-satoshis {
display: none;
text-align: right;
@media (min-width: 576px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 1100px) {
display: table-cell;
}
}
.table-cell-fiat {
display: none;
text-align: right;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: table-cell;
}
}
.table-cell-fees {
text-align: right;
}
}
.skeleton-loader-transactions {
max-width: 250px;
position: relative;
top: 2px;
margin-bottom: -3px;
height: 18px;
}
.lastest-blocks-table {
width: 100%;
text-align: left;
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
}
.table-cell-height {
width: 15%;
}
.table-cell-mined {
width: 35%;
text-align: left;
}
.table-cell-transaction-count {
display: none;
text-align: right;
width: 20%;
display: table-cell;
}
.table-cell-size {
display: none;
text-align: center;
width: 30%;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: table-cell;
}
}
}
.lastest-replacements-table {
width: 100%;
text-align: left;
table-layout:fixed;
tr, td, th {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.75rem !important;
}
td {
overflow:hidden;
width: 25%;
}
.table-cell-txid {
width: 25%;
text-align: start;
}
.table-cell-old-fee {
width: 25%;
text-align: end;
@media(max-width: 1080px) {
display: none;
}
}
.table-cell-new-fee {
width: 20%;
text-align: end;
}
.table-cell-badges {
width: 23%;
padding-right: 0;
padding-left: 5px;
text-align: end;
.badge {
margin-left: 5px;
}
}
}
.mempool-graph {
height: 255px;
@media (min-width: 768px) {
height: 285px;
}
@media (min-width: 992px) {
height: 370px;
}
}
.loadingGraphs{
height: 250px;
display: grid;
place-items: center;
}
.inc-tx-progress-bar {
max-width: 250px;
.progress-bar {
padding: 4px;
}
}
.terms-of-service {
margin-top: 1rem;
}
.small-bar {
height: 8px;
top: -4px;
max-width: 120px;
}
.loading-container {
min-height: 76px;
}
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.card-wrapper {
.card {
height: auto !important;
}
.card-body {
display: flex;
flex: inherit;
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 22px 20px;
&.liquid {
height: 124.5px;
}
}
.less-padding {
padding: 20px 20px;
}
}
.retarget-sign {
margin-right: -3px;
font-size: 14px;
top: -2px;
position: relative;
}
.previous-retarget-sign {
margin-right: -2px;
font-size: 10px;
}
.assetIcon {
width: 40px;
height: 40px;
}
.asset-title {
text-align: left;
vertical-align: middle;
}
.asset-icon {
width: 65px;
height: 65px;
vertical-align: middle;
}
.circulating-amount {
text-align: right;
width: 100%;
vertical-align: middle;
}
.clear-link {
color: white;
}
.pool-name {
display: inline-block;
vertical-align: text-top;
padding-left: 10px;
}
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
display: block;
margin-bottom: 10px;
text-decoration: none;
color: inherit;
}
.mempool-block-wrapper {
max-height: 410px;
max-width: 410px;
margin: auto;
@media (min-width: 768px) {
max-height: 344px;
max-width: 344px;
}
@media (min-width: 992px) {
max-height: 410px;
max-width: 410px;
}
}
.goggle-badge {
margin: 6px 5px 8px;
background: none;
border: solid 2px var(--primary);
cursor: pointer;
&.active {
background: var(--primary);
}
}
.btn-xs {
padding: 0.35rem 0.5rem;
font-size: 12px;
}
.quick-filter {
margin-top: 5px;
margin-bottom: 6px;
}
.card-liquid {
background-color: var(--bg);
height: 418px;
@media (min-width: 992px) {
height: 512px;
}
&.smaller {
height: 408px;
}
}
.card-title-liquid {
padding-top: 20px;
margin-left: 10px;
}
.in-progress-message {
position: relative;
color: #ffffff91;
margin-top: 20px;
text-align: center;
padding-bottom: 3px;
font-weight: 500;
}
.stats-card {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: var(--title-fg);
}
.card-text {
font-size: 18px;
span {
color: var(--transparent-fg);
font-size: 12px;
}
}
}
}

View File

@@ -0,0 +1,378 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface';
import { MempoolInfo, ReplacementInfo } from '../../interfaces/websocket.interface';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service';
import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../../shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils';
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
interface MempoolBlocksData {
blocks: number;
size: number;
}
interface MempoolInfoData {
memPoolInfo: MempoolInfo;
vBytesPerSecond: number;
progressWidth: string;
progressColor: string;
}
interface MempoolStatsData {
mempool: OptimizedMempoolStats[];
weightPerSecond: any;
}
@Component({
selector: 'app-custom-dashboard',
templateUrl: './custom-dashboard.component.html',
styleUrls: ['./custom-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewInit {
network$: Observable<string>;
mempoolBlocksData$: Observable<MempoolBlocksData>;
mempoolInfoData$: Observable<MempoolInfoData>;
mempoolLoadingStatus$: Observable<number>;
vBytesPerSecondLimit = 1667;
transactions$: Observable<TransactionStripped[]>;
blocks$: Observable<BlockExtended[]>;
replacements$: Observable<ReplacementInfo[]>;
latestBlockHeight: number;
mempoolTransactionsWeightPerSecondData: any;
mempoolStats$: Observable<MempoolStatsData>;
transactionsWeightPerSecondOptions: any;
isLoadingWebSocket$: Observable<boolean>;
isLoad: boolean = true;
filterSubscription: Subscription;
mempoolInfoSubscription: Subscription;
currencySubscription: Subscription;
currency: string;
incomingGraphHeight: number = 300;
graphHeight: number = 300;
webGlEnabled = true;
isMobile: boolean = window.innerWidth <= 767.98;
widgets;
addressSubscription: Subscription;
blockTxSubscription: Subscription;
addressSummary$: Observable<AddressTxSummary[]>;
address: Address;
goggleResolution = 82;
goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[], gradient: GradientMode }[] = [
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
];
goggleFlags = 0n;
goggleMode: FilterMode = 'and';
gradientMode: GradientMode = 'age';
goggleIndex = 0;
private destroy$ = new Subject();
constructor(
public stateService: StateService,
private apiService: ApiService,
private electrsApiService: ElectrsApiService,
private websocketService: WebsocketService,
private seoService: SeoService,
private cd: ChangeDetectorRef,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
this.widgets = this.stateService.env.customize?.dashboard.widgets || [];
}
ngAfterViewInit(): void {
this.stateService.focusSearchInputDesktop();
}
ngOnDestroy(): void {
this.filterSubscription.unsubscribe();
this.mempoolInfoSubscription.unsubscribe();
this.currencySubscription.unsubscribe();
this.websocketService.stopTrackRbfSummary();
if (this.addressSubscription) {
this.addressSubscription.unsubscribe();
this.websocketService.stopTrackingAddress();
this.address = null;
}
this.destroy$.next(1);
this.destroy$.complete();
}
ngOnInit(): void {
this.onResize();
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.seoService.resetTitle();
this.seoService.resetDescription();
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
this.websocketService.startTrackRbfSummary();
this.network$ = merge(of(''), this.stateService.networkChanged$);
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
.pipe(
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
);
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
const activeFilters = active.filters.sort().join(',');
for (const goggle of this.goggleCycle) {
if (goggle.mode === active.mode) {
const goggleFilters = goggle.filters.sort().join(',');
if (goggleFilters === activeFilters) {
this.goggleIndex = goggle.index;
this.goggleFlags = toFlags(goggle.filters);
this.goggleMode = goggle.mode;
this.gradientMode = active.gradient;
return;
}
}
}
this.goggleCycle.push({
index: this.goggleCycle.length,
name: 'Custom',
mode: active.mode,
filters: active.filters,
gradient: active.gradient,
});
this.goggleIndex = this.goggleCycle.length - 1;
this.goggleFlags = toFlags(active.filters);
this.goggleMode = active.mode;
});
this.mempoolInfoData$ = combineLatest([
this.stateService.mempoolInfo$,
this.stateService.vbytesPerSecond$
]).pipe(
map(([mempoolInfo, vbytesPerSecond]) => {
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
let progressColor = 'bg-success';
if (vbytesPerSecond > 1667) {
progressColor = 'bg-warning';
}
if (vbytesPerSecond > 3000) {
progressColor = 'bg-danger';
}
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
let mempoolSizeProgress = 'bg-danger';
if (mempoolSizePercentage <= 50) {
mempoolSizeProgress = 'bg-success';
} else if (mempoolSizePercentage <= 75) {
mempoolSizeProgress = 'bg-warning';
}
return {
memPoolInfo: mempoolInfo,
vBytesPerSecond: vbytesPerSecond,
progressWidth: percent + '%',
progressColor: progressColor,
mempoolSizeProgress: mempoolSizeProgress,
};
})
);
this.mempoolInfoSubscription = this.mempoolInfoData$.subscribe();
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
.pipe(
map((mempoolBlocks) => {
const size = mempoolBlocks.map((m) => m.blockSize).reduce((a, b) => a + b, 0);
const vsize = mempoolBlocks.map((m) => m.blockVSize).reduce((a, b) => a + b, 0);
return {
size: size,
blocks: Math.ceil(vsize / this.stateService.blockVSize)
};
})
);
this.transactions$ = this.stateService.transactions$;
this.blocks$ = this.stateService.blocks$
.pipe(
tap((blocks) => {
this.latestBlockHeight = blocks[0].height;
}),
switchMap((blocks) => {
if (this.stateService.env.MINING_DASHBOARD === true) {
for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template
block.extras.pool.logo = `/resources/mining-pools/` +
block.extras.pool.slug + '.svg';
}
}
return of(blocks.slice(0, 6));
})
);
this.replacements$ = this.stateService.rbfLatestSummary$;
this.mempoolStats$ = this.stateService.connectionState$
.pipe(
filter((state) => state === 2),
switchMap(() => this.apiService.list2HStatistics$().pipe(
catchError((e) => {
return of(null);
})
)),
switchMap((mempoolStats) => {
return merge(
this.stateService.live2Chart$
.pipe(
scan((acc, stats) => {
const now = Date.now() / 1000;
const start = now - (2 * 60 * 60);
acc.unshift(stats);
acc = acc.filter(p => p.added >= start);
return acc;
}, (mempoolStats || []))
),
of(mempoolStats)
);
}),
map((mempoolStats) => {
if (mempoolStats) {
return {
mempool: mempoolStats,
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
};
} else {
return null;
}
}),
shareReplay(1),
);
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
this.currency = fiat;
});
this.startAddressSubscription();
}
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
mempoolStats.reverse();
const labels = mempoolStats.map(stats => stats.added);
return {
labels: labels,
series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])],
};
}
trackByBlock(index: number, block: BlockExtended) {
return block.height;
}
getArrayFromNumber(num: number): number[] {
return Array.from({ length: num }, (_, i) => i + 1);
}
setFilter(index): void {
const selected = this.goggleCycle[index];
this.stateService.activeGoggles$.next(selected);
}
startAddressSubscription(): void {
if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.address)) {
let addressString = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address;
addressString = (/^[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(addressString)) ? addressString.toLowerCase() : addressString;
this.addressSubscription = (
addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(addressString)
: this.electrsApiService.getAddress$(addressString)
).pipe(
catchError((err) => {
console.log(err);
return of(null);
}),
filter((address) => !!address),
).subscribe((address: Address) => {
this.websocketService.startTrackAddress(address.address);
this.address = address;
this.cd.markForCheck();
});
this.addressSummary$ = (
addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getScriptHashSummary$((addressString.length === 66 ? '21' : '41') + addressString + 'ac')
: this.electrsApiService.getAddressSummary$(addressString)).pipe(
catchError(e => {
return of(null);
}),
switchMap(initial => this.stateService.blockTransactions$.pipe(
startWith(null),
scan((summary, tx) => {
if (tx && !summary.some(t => t.txid === tx.txid)) {
let value = 0;
let funded = 0;
let fundedCount = 0;
let spent = 0;
let spentCount = 0;
for (const vout of tx.vout) {
if (vout.scriptpubkey_address === addressString) {
value += vout.value;
funded += vout.value;
fundedCount++;
}
}
for (const vin of tx.vin) {
if (vin.prevout?.scriptpubkey_address === addressString) {
value -= vin.prevout?.value;
spent += vin.prevout?.value;
spentCount++;
}
}
if (this.address && this.address.address === addressString) {
this.address.chain_stats.tx_count++;
this.address.chain_stats.funded_txo_sum += funded;
this.address.chain_stats.funded_txo_count += fundedCount;
this.address.chain_stats.spent_txo_sum += spent;
this.address.chain_stats.spent_txo_count += spentCount;
}
summary.unshift({
txid: tx.txid,
time: tx.status?.block_time,
height: tx.status?.block_height,
value
});
}
return summary;
}, initial)
)),
share(),
);
}
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {
this.incomingGraphHeight = 300;
this.goggleResolution = 82;
this.graphHeight = 400;
} else if (window.innerWidth >= 768) {
this.incomingGraphHeight = 215;
this.goggleResolution = 80;
this.graphHeight = 310;
} else {
this.incomingGraphHeight = 180;
this.goggleResolution = 86;
this.graphHeight = 310;
}
this.isMobile = window.innerWidth <= 767.98;
}
}

View File

@@ -119,7 +119,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -1,9 +1,9 @@
.difficulty-tooltip {
position: fixed;
background: rgba(#11131f, 0.95);
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1;
color: var(--tooltip-grey);
padding: 10px 15px;
text-align: left;
pointer-events: none;

View File

@@ -15,8 +15,8 @@
<svg #epochSvg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<defs>
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#105fb0" />
<stop offset="100%" stop-color="#9339f4" />
<stop offset="0%" stop-color="var(--primary)" />
<stop offset="100%" stop-color="var(--mainnet-alt)" />
</linearGradient>
<linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#2486eb" />

View File

@@ -128,7 +128,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
@@ -223,7 +224,7 @@
height: 100%;
}
.background {
background: linear-gradient(to right, var(--primary), #9339f4);
background: linear-gradient(to right, var(--primary), var(--mainnet-alt));
left: 0;
right: 0;
}

View File

@@ -79,7 +79,7 @@
display: flex;
flex-direction: row;
transition: background-color 1s;
color: var(--color-fg);
color: #fff;
&.priority {
@media (767px < width < 992px), (width < 576px) {
width: 100%;

View File

@@ -16,8 +16,8 @@ export class FeesBoxComponent implements OnInit, OnDestroy {
isLoading$: Observable<boolean>;
recommendedFees$: Observable<Recommendedfees>;
themeSubscription: Subscription;
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
noPriority = '#2e324e';
gradient = 'linear-gradient(to right, var(--skeleton-bg), var(--skeleton-bg))';
noPriority = 'var(--skeleton-bg)';
fees: Recommendedfees;
constructor(

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -11,7 +11,8 @@
.main-title {
position: relative;
color: #ffffff91;
color: var(--fg);
opacity: var(--opacity);
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;

View File

@@ -66,7 +66,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
if (!this.data) {
return;
}
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
this.windowPreference = (this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference')) || '2h';
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
this.MA = this.calculateMA(this.data.series[0], windowSize);
if (this.outlierCappingEnabled === true) {
@@ -216,22 +216,19 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
type: 'line',
},
formatter: (params: any) => {
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
const bestItem = params.reduce((best, item) => {
return (item.seriesName === 'data' && (!best || best.value[1] < item.value[1])) ? item : best;
}, null);
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, bestItem.axisValue);
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`;
let itemFormatted = '<div class="title">' + axisValueLabel + '</div>';
params.map((item: any, index: number) => {
//Do no include MA in tooltip legend!
if (item.seriesName !== 'MA') {
if (index < 26) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
if (bestItem) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(bestItem.color)}</div>
<div class="grow"></div>
<div class="value">${formatNumber(item.value[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div>
<div class="value">${formatNumber(bestItem.value[1], this.locale, '1.0-0')}<span class="symbol">vB/s</span></div>
</div>`;
}
}
});
}
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}"
style="width: ${(this.windowPreference === '2h' || this.template === 'widget') ? '125px' : '215px'}">${itemFormatted}</div>`;
}

View File

@@ -1,5 +1,5 @@
<div [formGroup]="languageForm" class="text-small text-center">
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 95px;" (change)="changeLanguage()">
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 120px;" (change)="changeLanguage()">
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
</select>
</div>

View File

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

View File

@@ -4,6 +4,7 @@
top: 0;
width: 100%;
z-index: 100;
background-color: var(--bg);
}
li.nav-item.active {
@@ -17,7 +18,7 @@ fa-icon {
.navbar {
z-index: 100;
min-height: 64px;
background-color: var(--bg);
background-color: var(--nav-bg);
}
li.nav-item {
@@ -48,7 +49,7 @@ li.nav-item {
}
.navbar-nav {
background: var(--navbar-bg);
background: var(--nav-bg);
bottom: 0;
box-shadow: 0px 0px 15px 0px #000;
flex-direction: row;
@@ -169,4 +170,8 @@ nav {
margin-left: 5px;
margin-right: 0px;
}
}
.beta-network {
font-size: 8px;
}

View File

@@ -122,8 +122,8 @@ export class FederationUtxosListComponent implements OnInit {
getGradientColor(value: number): string {
const distanceToGreen = Math.abs(4032 - value);
const green = 'var(--green)';
const red = 'var(--red)';
const green = '#3bcc49';
const red = '#dc3545';
if (value < 0) {
return red;

View File

@@ -3,7 +3,7 @@
<h5 class="card-title" i18n="liquid.unpeg">Unpeg</h5>
<div *ngIf="(unbackedMonths$ | async) as unbackedMonths; else loadingData" class="card-text">
<ng-container *ngIf="unbackedMonths.historyComplete; else loadingData">
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.total > 0, 'correct': unbackedMonths.total === 0}">
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.total > 0, 'correct': unbackedMonths.total === 0}" i18n-ngbTooltip="liquid.unpeg-info" ngbTooltip="Number of times that the Federation's BTC holdings fall below 95% of the total L-BTC supply">
{{ unbackedMonths.total }} <span i18n="liquid.unpeg-event">Unpeg Event</span>
</div>
</ng-container>

View File

@@ -34,7 +34,7 @@ export class ReservesRatioStatsComponent implements OnInit {
let avg = 0;
for (let i = 0; i < ratioSeries.length; i++) {
avg += ratioSeries[i];
if (ratioSeries[i] < 1) {
if (ratioSeries[i] < 0.95) {
total++;
}
}

View File

@@ -85,6 +85,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
{
type: 'gauge',
startAngle: 180,
silent: true,
endAngle: 0,
center: ['50%', '75%'],
radius: '100%',

View File

@@ -2,13 +2,21 @@
<div class="preview-wrapper">
<header>
<span class="header-brand" style="position: relative;">
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images>
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
@if (enterpriseInfo?.img) {
<img [src]="enterpriseInfo.img" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
}
@if (enterpriseInfo?.header_img) {
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="60px" class="mr-3">
} @else {
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images>
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
}
</span>
<div [ngSwitch]="network.val">
<span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet</span>
<span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span>
<span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet3</span>
<span *ngSwitchCase="'testnet4'" class="network testnet"><app-svg-images name="testnet4" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet4</span>
<span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span>
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span>
<span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span>

View File

@@ -5,6 +5,7 @@
max-width: 1200px;
max-height: 600px;
padding-top: 80px;
background: var(--nav-bg);
header {
position: absolute;
@@ -18,7 +19,7 @@
flex-direction: row;
justify-content: space-between;
align-items: center;
background: var(--active-bg);
background: var(--nav-bg);
text-align: start;
font-size: 1.8em;
}
@@ -77,3 +78,15 @@
flex-shrink: 1;
}
}
.subdomain_logo {
height: 35px;
overflow: clip;
max-width: 140px;
margin: auto;
align-self: center;
margin-right: 1em;
.rounded {
border-radius: 5px;
}
}

View File

@@ -1,7 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Observable, merge, of } from 'rxjs';
import { Observable, Subscription, merge, of } from 'rxjs';
import { LanguageService } from '../../services/language.service';
import { EnterpriseService } from '../../services/enterprise.service';
@Component({
selector: 'app-master-page-preview',
@@ -13,15 +14,23 @@ export class MasterPagePreviewComponent implements OnInit {
lightning$: Observable<boolean>;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
urlLanguage: string;
subdomain = '';
enterpriseInfo: any;
enterpriseInfo$: Subscription;
constructor(
public stateService: StateService,
private languageService: LanguageService,
private enterpriseService: EnterpriseService,
) { }
ngOnInit() {
this.network$ = merge(of(''), this.stateService.networkChanged$);
this.lightning$ = this.stateService.lightningChanged$;
this.urlLanguage = this.languageService.getLanguageForUrl();
this.subdomain = this.enterpriseService.getSubdomain();
this.enterpriseInfo$ = this.enterpriseService.info$.subscribe(info => {
this.enterpriseInfo = info;
});
}
}

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