Compare commits

...

191 Commits

Author SHA1 Message Date
52e1919aba
Merge pull request #5 from Retropex/rebase0.3.2
Rebase to `v0.3.2`
2025-04-07 10:50:23 +02:00
37bb76eedb
fix coinbase audit 2025-04-07 10:07:01 +02:00
ba291cd697
remove shitcoin explorer 2025-04-07 10:07:01 +02:00
14dba2ef49
rename data to spam 2025-04-07 10:07:01 +02:00
3772f37516
remove trade mark 2025-04-07 10:07:01 +02:00
2b56b90169
reworked health audit 2025-04-07 10:06:55 +02:00
wiz
74dde85112
Release v3.2.0 2025-04-07 14:17:34 +09:00
mononaut
69fe083fc3
Merge pull request #5856 from mempool/knorrium/automatic_pool_updates
Set automatic pool updates on by default
2025-04-07 12:48:22 +08:00
wiz
7dab8f19a2
docker: Add missing nginx route for /api/v1/services 2025-04-06 23:44:06 +09:00
Felipe Knorr Kuhn
c5f7d56113
Set automatic pool updates on by default 2025-04-06 23:33:53 +09:00
wiz
6946878caf
Merge pull request #5855 from mempool/knorrium/pin_node22
Pin node 22 to 22.14.0
2025-04-06 21:59:27 +09:00
Felipe Knorr Kuhn
a2bcaac682
Merge branch 'master' into knorrium/pin_node22 2025-04-06 18:39:22 +09:00
wiz
193cc73df5
Merge pull request #5854 from mempool/mononaut/fix-unix-sockets
Fix axios unix sockets
2025-04-06 18:07:27 +09:00
Felipe Knorr Kuhn
84c9a01b6d
Pin Node versions to 22.14.0 on the Docker images 2025-04-06 18:03:03 +09:00
Felipe Knorr Kuhn
3df953c0a8
Pin GitHub actions to node 22.14.0 2025-04-06 18:02:22 +09:00
wiz
dc9152edcb
Merge branch 'master' into mononaut/fix-unix-sockets 2025-04-06 18:00:27 +09:00
Mononaut
849eebe583
Fix axios unix sockets 2025-04-06 08:59:06 +00:00
wiz
b73f93e1cd
ops: Use gcc to build NodeJS v22 on FreeBSD 14 2025-04-06 17:55:49 +09:00
wiz
c760de1517
Merge pull request #5853 from mempool/mononaut/pump-it-up
pump up monitoring frequency
2025-04-06 16:03:06 +09:00
Mononaut
05d2aa73f8
pump up monitoring frequency 2025-04-06 06:55:27 +00:00
wiz
ef6ce8d295
Merge pull request #5838 from mempool/mononaut/polish-pool-updates
polish pool updates
2025-04-06 15:41:18 +09:00
mononaut
9ec9365757
Merge pull request #5824 from mempool/simon/fix-database-disabled
Fix database disabled
2025-04-06 14:26:57 +08:00
wiz
5a3f2d5532
Merge pull request #5852 from mempool/knorrium/bump_ver
Bump package.json versions ahead of the official release
2025-04-06 15:23:52 +09:00
Felipe Knorr Kuhn
08a40fc61d
Merge branch 'master' into knorrium/bump_ver 2025-04-06 13:58:46 +09:00
mononaut
a93962be9f
Merge pull request #5850 from mempool/natsoni/pool-page
FIx pool page CSS
2025-04-06 09:45:11 +08:00
Felipe Knorr Kuhn
22af7de5bd
Bump package.json versions ahead of the official release 2025-04-05 20:03:14 +09:00
Felipe Knorr Kuhn
fcebe001d8
Merge branch 'master' into simon/fix-database-disabled 2025-04-04 06:46:17 -07:00
Felipe Knorr Kuhn
4782b1f77e
Merge branch 'master' into natsoni/pool-page 2025-04-04 06:25:52 -07:00
natsoni
d3d7829fee
FIx pool page CSS 2025-04-03 18:00:45 +02:00
wiz
de8706b3b9
Merge pull request #5847 from mempool/knorrium/e2e_rbf_page 2025-04-04 00:14:34 +09:00
wiz
1baae6474b
ops: Bump elements to v23.2.7 2025-04-03 19:19:55 +09:00
Felipe Knorr Kuhn
82a5010dfb
Merge branch 'master' into knorrium/e2e_rbf_page 2025-04-02 17:55:42 -07:00
wiz
9b214f7bae
Merge pull request #5845 from mempool/knorrium/node22
Bump node version to v22
2025-04-02 20:16:15 +09:00
wiz
1790c83bab
ops: Bump NodeJS to v22 for install and start scripts 2025-04-02 20:08:09 +09:00
Felipe Knorr Kuhn
05e407ff0f
Add a test for the RBF page updates 2025-04-02 19:26:46 +09:00
Felipe Knorr Kuhn
0d85d9429b
Merge branch 'knorrium/node22' of github.com:mempool/mempool into knorrium/node22 2025-04-02 15:52:15 +09:00
Felipe Knorr Kuhn
561f803855
Merge branch 'master' into knorrium/node22 2025-04-02 15:50:16 +09:00
Felipe Knorr Kuhn
f41c9a0e57
Update missing node matrix 2025-04-02 15:45:21 +09:00
Felipe Knorr Kuhn
f1228a6c28
Merge branch 'master' into knorrium/node22 2025-04-01 23:44:17 -07:00
wiz
9c4278834f
Merge pull request #5842 from mempool/mononaut/tighten-blockchain-screws
Fix loose screws on the blockchain bar
2025-04-02 14:06:30 +09:00
Felipe Knorr Kuhn
f0224b0bf0
Bump node version to v22 2025-04-02 13:42:25 +09:00
Felipe Knorr Kuhn
0b9c11dcca
Merge pull request #5844 from mempool/knorrium/run_tests_after_merging
Run CI workflow after merging too
2025-04-01 21:38:58 -07:00
Felipe Knorr Kuhn
42d4cb3cc5
Run CI workflow after merging too 2025-04-02 13:29:39 +09:00
Felipe Knorr Kuhn
00769edd97
Merge pull request #5843 from mempool/knorrium/e2e_rbf_and_poisoning_tests
Add E2E tests for address poisoning highlighting and the RBF tx tracker
2025-04-01 20:38:33 -07:00
Felipe Knorr Kuhn
7712034c84
Add another test for address poisioning 2025-04-02 12:19:00 +09:00
Felipe Knorr Kuhn
35d7b1c71d
Import the missing ws function 2025-04-02 11:25:40 +09:00
Felipe Knorr Kuhn
1d2d9c0be6
Add an address poisoning attack test 2025-04-02 11:25:14 +09:00
Felipe Knorr Kuhn
a0e41b31e0
Add a RBF tx tracker test 2025-04-02 11:24:59 +09:00
Felipe Knorr Kuhn
b6ad17b995
Add accelerator fixture 2025-04-02 11:19:32 +09:00
Felipe Knorr Kuhn
20ef5b3288
Add fixtures for the RBF tx tracker test 2025-04-02 11:19:06 +09:00
Felipe Knorr Kuhn
c43f436b04
Add a new WS mocking utility to send fixtures or single messages 2025-04-02 11:18:12 +09:00
Mononaut
2235d6305f
Fix loose screws on the blockchain bar 2025-04-02 00:28:17 +00:00
Mononaut
9f5c654b52
clamp min pool update delay 2025-04-01 11:31:37 +00:00
wiz
3c08b5c72b
Merge pull request #5840 from mempool/mononaut/0402
more misc changes
2025-04-01 18:03:27 +09:00
Mononaut
a0946ab046
more misc changes 2025-04-01 08:52:36 +00:00
wiz
9269e872f4
Merge pull request #5837 from mempool/mononaut/0401
misc changes
2025-04-01 16:52:12 +09:00
Mononaut
ab5c49ea7a
Reset block cache after updating pools 2025-04-01 07:34:49 +00:00
Mononaut
1c9b422db4
fix pool update retry delay 2025-04-01 06:42:05 +00:00
Mononaut
3500a657a9
misc changes 2025-04-01 06:23:36 +00:00
wiz
4d8a68156d
Merge pull request #5835 from mempool/knorrium/e2e_pr_or_branch 2025-04-01 11:08:58 +09:00
Felipe Knorr Kuhn
6533c19bd1
Add support for running tests against any PR or branch 2025-04-01 09:23:52 +09:00
wiz
02034f0bd5
Merge pull request #5832 from mempool/knorrium/e2e_updates 2025-03-31 23:20:35 +09:00
Felipe Knorr Kuhn
d57b9ac9a4
Fix running only one test 2025-03-31 22:56:33 +09:00
Felipe Knorr Kuhn
860ca9698f
Fix failing RBF test on mobile 2025-03-31 22:47:14 +09:00
Felipe Knorr Kuhn
9f6d0a6dcb
Add new parameterized e2e dispatch workflow 2025-03-31 22:11:14 +09:00
Felipe Knorr Kuhn
57765e4ee6
Update e2e task with the new target 2025-03-31 22:10:58 +09:00
Felipe Knorr Kuhn
f56e748c0d
Replace staging with parameterized 2025-03-31 22:10:44 +09:00
Felipe Knorr Kuhn
7c682d8be9
Add parameterized proxy 2025-03-31 22:05:52 +09:00
Felipe Knorr Kuhn
c305228530
Revert mempool-ci runners 2025-03-31 22:02:52 +09:00
wiz
35557cf008
Merge pull request #5829 from mempool/knorrium/ci_runner
Use github-hosted runners
2025-03-31 17:09:30 +09:00
Felipe Knorr Kuhn
d805304d79
Use github-hosted runners 2025-03-31 16:14:46 +09:00
nymkappa
18b354f5d1
Merge pull request #5722 from mempool/translations_frontend-src-locale-messages-xlf--master_fr 2025-03-29 20:16:48 +01:00
wiz
272bcfc57d
Merge pull request #5787 from mempool/hunicus/mempool-tm
Add Mempool to trademark guidelines
2025-03-29 17:53:06 +09:00
wiz
329a51d455
Merge branch 'master' into hunicus/mempool-tm 2025-03-29 17:47:02 +09:00
wiz
ad5b804a4d
Merge pull request #5791 from mempool/simon/deprecating-tv-view
Removing the (broken) tv view
2025-03-29 17:46:56 +09:00
wiz
1750b08254
Merge branch 'master' into hunicus/mempool-tm 2025-03-29 17:46:50 +09:00
wiz
7092a77926
Merge branch 'master' into simon/deprecating-tv-view 2025-03-29 17:43:33 +09:00
wiz
8b9050039b
Merge pull request #5772 from mempool/nymkappa/cors
fix cors
2025-03-29 17:43:24 +09:00
wiz
3bdcfe7307
Merge branch 'master' into simon/deprecating-tv-view 2025-03-29 17:41:51 +09:00
wiz
87978921d3
Merge pull request #5783 from mempool/hunicus/fortis-enterprise
Add fortris to enterprise sponsors
2025-03-29 17:41:42 +09:00
wiz
ecf81c8f17
Merge pull request #5804 from mempool/hunicus/memerator-r
Mark mempool accelerator as registered trademark
2025-03-29 17:41:13 +09:00
wiz
56fd407cdd
Merge branch 'master' into simon/deprecating-tv-view 2025-03-29 17:40:15 +09:00
wiz
244cdcd46b
Merge pull request #5807 from mempool/nymkappa/fix-typo
retreive -> retrieve
2025-03-29 17:38:19 +09:00
wiz
b261001e79
Merge pull request #5813 from mempool/nymkappa/accelerator-receipt
[accelerator] show square receipt if available
2025-03-29 17:38:03 +09:00
orangesurf
583a11b75f
Merge branch 'master' into hunicus/fortis-enterprise 2025-03-29 17:34:27 +09:00
mononaut
39e7aaef09
Merge branch 'master' into nymkappa/accelerator-receipt 2025-03-29 16:33:50 +08:00
wiz
a427aa2058
Merge branch 'master' into hunicus/memerator-r 2025-03-29 17:33:11 +09:00
wiz
c9a4295b8f
Merge pull request #5769 from mempool/mononaut/address-antidote
detect and warn about address poisoning attacks
2025-03-29 17:30:39 +09:00
mononaut
ec99dc781e
Merge pull request #5815 from mempool/natsoni/tx-preview-feedback
Tx preview: trim input and redirect to tx page
2025-03-29 16:30:27 +08:00
Mononaut
fb50ea7a6d
detect and warn about address poisoning attacks 2025-03-29 08:27:20 +00:00
mononaut
172498389e
Merge branch 'master' into natsoni/tx-preview-feedback 2025-03-29 16:15:05 +08:00
mononaut
5a56d560d8
Merge pull request #5794 from mempool/mononaut/balance-chart-label
Add configurable label to balance widget
2025-03-29 16:14:22 +08:00
Mononaut
3056454389
Add configurable label to balance widget 2025-03-29 08:09:56 +00:00
mononaut
5c02670281
Merge branch 'master' into natsoni/tx-preview-feedback 2025-03-29 16:06:55 +08:00
wiz
293e019b83
Merge pull request #5810 from mempool/mononaut/update-custom-dash-config
update custom dashboard config
2025-03-29 16:07:47 +09:00
wiz
3c9b09b328
Merge pull request #5809 from mempool/mononaut/autofetch-wallets
automatically fetch enabled wallets from services backend
2025-03-29 15:42:20 +09:00
Mononaut
072b83243e
update custom dashboard config 2025-03-29 05:46:35 +00:00
Mononaut
b153d21162
automatically fetch enabled wallets from services backend 2025-03-29 05:45:22 +00:00
mononaut
9079a9db8f
Merge pull request #5817 from mempool/natsoni/tapscript-multisig-feedback
Tapscript multisig parsing feedback
2025-03-29 10:52:49 +08:00
mononaut
426821c40f
Merge branch 'master' into natsoni/tapscript-multisig-feedback 2025-03-29 10:49:00 +08:00
mononaut
8981c77069
Merge pull request #5826 from mempool/nymkappa/fix-block-api-response-code
[`/api/v1/block/:hash`] respect 404 error code instead of misleading 500
2025-03-29 10:48:36 +08:00
mononaut
d4c66a6231
Merge branch 'master' into natsoni/tapscript-multisig-feedback 2025-03-29 10:47:47 +08:00
mononaut
ac2d6d0186
Merge branch 'master' into nymkappa/fix-block-api-response-code 2025-03-29 10:43:22 +08:00
nymkappa
2945e47eba
[blocks] respect 404 error code instead of misleading 500 2025-03-26 23:05:43 +01:00
softsimon
54cf5ea75e
Fix database diabled 2025-03-25 23:04:52 +07:00
wiz
30003348ce
ops: Fix premature socket close bug in nginx cache warmer scripts 2025-03-16 12:49:48 +09:00
natsoni
322e81d3ed
Parse tapscript unanimous n-of-n multisig 2025-03-14 14:59:15 +01:00
wiz
8b70e65717
Merge pull request #5818 from mempool/knorrium/no_latest_on_master 2025-03-14 14:57:15 +09:00
wiz
4d89cb01bd
ops: Tweak delay times for bitcoin.crontab 2025-03-14 13:34:09 +09:00
Felipe Knorr Kuhn
76f31623fe
Don't tag as latest by default 2025-03-13 15:49:45 -07:00
natsoni
188096e651
Add opcodes that can be used for tapscript multisig 2025-03-13 16:38:53 +01:00
natsoni
e94dc67b31
Update tapscript multisig minimum size 2025-03-13 15:46:11 +01:00
wiz
8efea61601
ops: Add electrs popular-scripts cron jobs 2025-03-13 09:49:40 +09:00
hunicus
00e7e726bc Mark mempool accelerator as registered trademark 2025-03-12 20:13:46 -04:00
natsoni
4dff3adf11
Redirect to tx page 2 seconds after broadcast 2025-03-12 16:49:38 +01:00
natsoni
062c5ca03a
Trim input data in tx preview 2025-03-12 16:47:31 +01:00
nymkappa
1121377c7a
[accelerator] add missing response from cashapp payment 2025-03-12 15:27:00 +01:00
nymkappa
cac404ae9b
[accelerator] make sure we cannot go back from 'success' step 2025-03-12 15:26:44 +01:00
nymkappa
7d9e275803
[accelerator] show square receipt if available 2025-03-12 14:56:15 +01:00
wiz
a152afb4af
ops: Fix nginx conf for elements unix socket paths 2025-03-12 11:51:03 +09:00
wiz
305d931d5c
ops: Add more sites to check script 2025-03-12 09:43:00 +09:00
wiz
65b276678f
ops: Add negative balance check to check script 2025-03-12 09:35:55 +09:00
wiz
c79ef93413
ops: Bump prod to bitcoin v28.1 + elements 23.2.6 2025-03-11 16:38:26 +09:00
wiz
9bef19449f
ops: Comment out old keybase commands in build script 2025-03-11 16:37:14 +09:00
wiz
636b4c0da7
ops: Add missing if check for CLN in prod install 2025-03-11 15:46:03 +09:00
wiz
7adc0083af
ops: Modify prod install to run even if mysql exists 2025-03-11 14:08:51 +09:00
wiz
658151e0e8
ops: Update electrs patch path for FreeBSD prod build 2025-03-11 12:46:00 +09:00
nymkappa
9d711c336a
retreive -> retrieve 2025-03-09 11:42:53 +01:00
softsimon
bc7230a5ef
Merge pull request #5805 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.13.0
Bump mysql2 from 3.12.0 to 3.13.0 in /backend
2025-03-09 10:51:17 +07:00
softsimon
2de5379be9
Merge pull request #5664 from mempool/natsoni/decode-tx
Decode transaction from hex
2025-03-09 10:39:39 +07:00
softsimon
9a4b5fda65
Merge pull request #5799 from mempool/natsoni/psbt-support
PSBT support in transaction preview
2025-03-08 20:27:53 +07:00
dependabot[bot]
3b9d9864cf
Bump mysql2 from 3.12.0 to 3.13.0 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.12.0 to 3.13.0.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.12.0...v3.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 02:22:43 +00:00
softsimon
442b6d1dad
Merge pull request #5800 from mempool/natsoni/taproot-multisig-detection
Tapscript multisig parsing
2025-03-06 17:04:10 +07:00
softsimon
5a96ccab63
Merge pull request #5803 from mempool/mononaut/change-staging-proxy
change staging proxy from fmt to va1
2025-03-06 10:54:28 +07:00
Mononaut
0b1895664b
change staging proxy from fmt to va1 2025-03-06 03:52:20 +00:00
wiz
b70cada60c
Merge pull request #5802 from mempool/mononaut/adjust-trademarks 2025-03-05 17:38:14 -10:00
wiz
4538b2570e
Merge pull request #5801 from mempool/mononaut/self-host-link 2025-03-05 17:37:47 -10:00
Mononaut
caef3c49a6
be your own explorer faq 2025-03-06 02:44:45 +00:00
Mononaut
55c09efb58
be your own explorer 2025-03-06 02:42:17 +00:00
natsoni
ad140dc60a
Tapscript multisig parsing 2025-03-05 15:33:57 +01:00
softsimon
8b435b7aa3
Merge pull request #5798 from mempool/mononaut/fix-audits-timestamp
disabled ON UPDATE for blocks_audits time field
2025-03-05 11:41:10 +07:00
softsimon
3695d8bc6b
Merge pull request #5795 from mempool/dependabot/npm_and_yarn/backend/axios-1.8.1
Bump axios from 1.7.2 to 1.8.1 in /backend
2025-03-05 11:38:11 +07:00
Mononaut
c4e22a6225
disabled ON UPDATE for blocks_audits time field 2025-03-05 04:18:31 +00:00
wiz
b8cddd71f6
Merge pull request #5796 from mempool/knorrium/tweaked_workflow
Update Docker images and Github workflow
2025-03-04 09:34:44 -10:00
wiz
494be165ad
Update latest tag on dockerhub 2025-03-04 09:25:39 -10:00
natsoni
c01e11899c
PSBT support in transaction preview 2025-03-04 16:43:41 +01:00
Felipe Knorr Kuhn
9c358060aa
Add dispatch workflow to update the latest tag 2025-02-28 23:22:00 -08:00
Felipe Knorr Kuhn
5116da2626
Do not update the latest tag when building 2025-02-28 23:20:40 -08:00
dependabot[bot]
cfe7c93755
Bump axios from 1.7.2 to 1.8.1 in /backend
Bumps [axios](https://github.com/axios/axios) from 1.7.2 to 1.8.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-27 02:13:31 +00:00
Felipe Knorr Kuhn
e6f13766d3
Update Docker images 2025-02-25 19:05:28 -08:00
Felipe Knorr Kuhn
d82a9f6c6a
Tweak Docker workflow 2025-02-25 18:56:29 -08:00
softsimon
650c3d949d
remove tv mode tests 2025-02-22 22:25:52 +07:00
softsimon
e40ca40ecb
Remove tv icon dep 2025-02-20 22:11:45 +07:00
softsimon
6ec1cc3fd5
Deprecating the tv view 2025-02-20 22:08:14 +07:00
wiz
f7d0d7a882
Merge pull request #5790 from mempool/mononaut/twidget
twidget
2025-02-19 17:11:32 -08:00
wiz
bdeaa466ef
Merge branch 'master' into mononaut/twidget 2025-02-19 17:07:55 -08:00
Mononaut
7671600455
temporary twidget mirror 2025-02-19 17:07:23 +00:00
wiz
c626bd1ea2
ops: Remove old X-Frame-Options HTTP header 2025-02-19 10:56:31 -06:00
wiz
b22bceb349
Merge pull request #5788 from mempool/mononaut/update-pools-more-often 2025-02-12 11:19:25 -10:00
Mononaut
d3b5c15f33
[ops] check for pool updates every hour 2025-02-12 15:03:52 +00:00
hunicus
c47af1f8b2
Add Mempool® to trademark guidelines 2025-02-11 15:30:51 -05:00
wiz
80201c0821
ops: Add new Bitcoin nodes in SG1 and HNL to bitcoin.conf 2025-02-10 09:33:07 -10:00
wiz
05536a05f0
Merge pull request #5786 from mempool/mononaut/fix-block-preview
misc unfurl preview fixes
2025-02-10 07:43:27 -10:00
natsoni
4c20d2b180
Move broadcast button to alert banner 2025-02-10 15:33:21 +01:00
natsoni
831b923dda
Update transaction preview messages 2025-02-10 15:32:16 +01:00
Mononaut
27c28f939c
misc unfurl preview fixes 2025-02-10 03:47:05 +00:00
natsoni
6340dc571c
Don't show ETA on unbroadcasted txs, and placeholder for missing fee 2025-02-09 18:13:06 +01:00
natsoni
4e45b55c3a
Merge remote-tracking branch 'origin/master' into natsoni/decode-tx 2025-02-09 18:08:00 +01:00
hunicus
3691eda9d1
Add fortris to enterprise sponsors 2025-02-08 18:51:03 -05:00
softsimon
1f4c56c19a
Merge pull request #5780 from mempool/natsoni/fix-scrollable-blockchain-ltr
Fix left-to-right scrollable blockchain
2025-02-08 13:03:52 +07:00
mononaut
15638896af
Merge pull request #5779 from mempool/nymkappa/new-fa-icon
add new fa icon
2025-02-08 11:44:05 +07:00
natsoni
1779c672e3
Don't tweak scrollLeft if time is left to right 2025-02-07 16:52:46 +01:00
nymkappa
5e0cbb084a
add new fa icon 2025-02-07 11:34:18 +01:00
nymkappa
aad4783415
fix cors 2025-02-04 21:33:21 +01:00
transifex-integration[bot]
a62a3cc774
Translate frontend/src/locale/messages.xlf in fr
100% reviewed source file: 'frontend/src/locale/messages.xlf'
on 'fr'.
2025-01-23 06:29:40 +00:00
transifex-integration[bot]
9660723e96
Translate frontend/src/locale/messages.xlf in fr
100% reviewed source file: 'frontend/src/locale/messages.xlf'
on 'fr'.
2025-01-13 02:39:48 +00:00
natsoni
860bc7d14d
Merge calculateMempoolTxCpfp and calculateLocalTxCpfp 2025-01-09 14:35:34 +01:00
natsoni
6c95cd2149
Update local cpfp API to accept array of transactions 2025-01-09 12:08:48 +01:00
natsoni
af0c78be81
Handle error from bitcoin client when querying prevouts 2025-01-08 15:25:41 +01:00
natsoni
5b331c144b
P2A address format decoding 2025-01-08 15:25:19 +01:00
Mononaut
74fa3c7eb1
conform getPrevouts and getCpfpLocalTx to new error handling standard 2025-01-01 16:59:47 +00:00
mononaut
e05a9a6dfa
Merge branch 'master' into natsoni/decode-tx 2025-01-01 09:00:19 -06:00
softsimon
80b6fd4a1b
Merge branch 'master' into natsoni/decode-tx 2024-12-25 22:45:41 +07:00
natsoni
2987f86cd3
Compute decoded tx CPFP data in the backend 2024-12-19 11:23:07 +01:00
natsoni
d852c48370
Move 'related transactions' to dedicated component 2024-12-19 11:22:41 +01:00
natsoni
727f22bc9d
Add backend endpoint to fetch prevouts 2024-12-15 19:39:32 +01:00
natsoni
e848d711fc
Merge branch 'master' into natsoni/decode-tx 2024-12-09 12:11:50 +01:00
natsoni
74ecd1aaac
Fix missing prevouts message 2024-11-28 14:32:20 +01:00
natsoni
722eaa3e96
Add note on borrowed code used for transaction decoding 2024-11-28 12:07:05 +01:00
natsoni
025b0585b4
Preview transaction from raw data 2024-11-27 20:37:52 +01:00
natsoni
2de16322ae
Utils functions for decoding tx client side 2024-11-27 17:54:07 +01:00
147 changed files with 6780 additions and 799 deletions

View File

@ -3,16 +3,19 @@ name: CI Pipeline for the Backend and Frontend
on:
pull_request:
types: [opened, review_requested, synchronize]
push:
branches:
- master
jobs:
backend:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'"
strategy:
matrix:
node: ["20", "21"]
node: ["22.14.0"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }}
steps:
@ -66,7 +69,7 @@ jobs:
cache:
name: "Cache assets for builds"
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
@ -157,13 +160,13 @@ jobs:
frontend:
needs: cache
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'"
strategy:
matrix:
node: ["20", "21"]
node: ["22.14.0"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }}
steps:
@ -245,8 +248,8 @@ jobs:
VERBOSE: 1
e2e:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'"
runs-on: ubuntu-latest
needs: frontend
strategy:
fail-fast: false
@ -263,7 +266,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 22
cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
@ -309,7 +312,7 @@ jobs:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
@ -334,7 +337,7 @@ jobs:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
@ -359,7 +362,7 @@ jobs:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:mempool
start: npm run start:local-staging
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
@ -375,10 +378,9 @@ jobs:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
validate_docker_json:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'"
runs-on: ubuntu-latest
name: Validate generated backend Docker JSON
steps:

View File

@ -0,0 +1,181 @@
name: Docker - Update latest tag
on:
workflow_dispatch:
inputs:
tag:
description: 'The Docker image tag to pull'
required: true
type: string
jobs:
retag-and-push:
strategy:
matrix:
service:
- frontend
- backend
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
id: buildx
with:
install: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get source image manifest and SHAs
id: source-manifest
run: |
set -e
echo "Fetching source manifest..."
MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:${{ github.event.inputs.tag }})
if [ -z "$MANIFEST" ]; then
echo "No manifest found. Assuming single-arch image."
exit 1
fi
echo "Original source manifest:"
echo "$MANIFEST" | jq .
AMD64_SHA=$(echo "$MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest')
ARM64_SHA=$(echo "$MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="arm64" and .platform.os=="linux") | .digest')
if [ -z "$AMD64_SHA" ] || [ -z "$ARM64_SHA" ]; then
echo "Source image is not multi-arch (missing amd64 or arm64)"
exit 1
fi
echo "Source amd64 manifest digest: $AMD64_SHA"
echo "Source arm64 manifest digest: $ARM64_SHA"
echo "amd64_sha=$AMD64_SHA" >> $GITHUB_OUTPUT
echo "arm64_sha=$ARM64_SHA" >> $GITHUB_OUTPUT
- name: Pull and retag architecture-specific images
run: |
set -e
docker buildx inspect --bootstrap
# Remove any existing local images to avoid cache interference
echo "Removing existing local images if they exist..."
docker image rm ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:${{ github.event.inputs.tag }} || true
# Pull amd64 image by digest
echo "Pulling amd64 image by digest..."
docker pull --platform linux/amd64 ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }}
PULLED_AMD64_MANIFEST_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} --format '{{index .RepoDigests 0}}' | cut -d@ -f2)
PULLED_AMD64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} --format '{{.Id}}')
echo "Pulled amd64 manifest digest: $PULLED_AMD64_MANIFEST_DIGEST"
echo "Pulled amd64 image ID (sha256): $PULLED_AMD64_IMAGE_ID"
# Pull arm64 image by digest
echo "Pulling arm64 image by digest..."
docker pull --platform linux/arm64 ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }}
PULLED_ARM64_MANIFEST_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} --format '{{index .RepoDigests 0}}' | cut -d@ -f2)
PULLED_ARM64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} --format '{{.Id}}')
echo "Pulled arm64 manifest digest: $PULLED_ARM64_MANIFEST_DIGEST"
echo "Pulled arm64 image ID (sha256): $PULLED_ARM64_IMAGE_ID"
# Tag the images
docker tag ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64
docker tag ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64
# Verify tagged images
TAGGED_AMD64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 --format '{{.Id}}')
TAGGED_ARM64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 --format '{{.Id}}')
echo "Tagged amd64 image ID (sha256): $TAGGED_AMD64_IMAGE_ID"
echo "Tagged arm64 image ID (sha256): $TAGGED_ARM64_IMAGE_ID"
- name: Push architecture-specific images
run: |
set -e
echo "Pushing amd64 image..."
docker push ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64
PUSHED_AMD64_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 --format '{{index .RepoDigests 0}}' | cut -d@ -f2)
echo "Pushed amd64 manifest digest (local): $PUSHED_AMD64_DIGEST"
# Fetch manifest from registry after push
echo "Fetching pushed amd64 manifest from registry..."
PUSHED_AMD64_REGISTRY_MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64)
PUSHED_AMD64_REGISTRY_DIGEST=$(echo "$PUSHED_AMD64_REGISTRY_MANIFEST" | jq -r '.config.digest')
echo "Pushed amd64 manifest digest (registry): $PUSHED_AMD64_REGISTRY_DIGEST"
echo "Pushing arm64 image..."
docker push ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64
PUSHED_ARM64_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 --format '{{index .RepoDigests 0}}' | cut -d@ -f2)
echo "Pushed arm64 manifest digest (local): $PUSHED_ARM64_DIGEST"
# Fetch manifest from registry after push
echo "Fetching pushed arm64 manifest from registry..."
PUSHED_ARM64_REGISTRY_MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64)
PUSHED_ARM64_REGISTRY_DIGEST=$(echo "$PUSHED_ARM64_REGISTRY_MANIFEST" | jq -r '.config.digest')
echo "Pushed arm64 manifest digest (registry): $PUSHED_ARM64_REGISTRY_DIGEST"
- name: Create and push multi-arch manifest with original digests
run: |
set -e
echo "Creating multi-arch manifest with original digests..."
docker manifest create ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest \
${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} \
${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }}
echo "Pushing multi-arch manifest..."
docker manifest push ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest
- name: Clean up intermediate tags
if: success()
run: |
docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 || true
docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 || true
docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:${{ github.event.inputs.tag }} || true
- name: Verify final manifest
run: |
set -e
echo "Fetching final generated manifest..."
FINAL_MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest)
echo "Generated final manifest:"
echo "$FINAL_MANIFEST" | jq .
FINAL_AMD64_SHA=$(echo "$FINAL_MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest')
FINAL_ARM64_SHA=$(echo "$FINAL_MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="arm64" and .platform.os=="linux") | .digest')
echo "Final amd64 manifest digest: $FINAL_AMD64_SHA"
echo "Final arm64 manifest digest: $FINAL_ARM64_SHA"
# Compare all digests
echo "Comparing digests..."
echo "Source amd64 digest: ${{ steps.source-manifest.outputs.amd64_sha }}"
echo "Pulled amd64 manifest digest: $PULLED_AMD64_MANIFEST_DIGEST"
echo "Pushed amd64 manifest digest (local): $PUSHED_AMD64_DIGEST"
echo "Pushed amd64 manifest digest (registry): $PUSHED_AMD64_REGISTRY_DIGEST"
echo "Final amd64 digest: $FINAL_AMD64_SHA"
echo "Source arm64 digest: ${{ steps.source-manifest.outputs.arm64_sha }}"
echo "Pulled arm64 manifest digest: $PULLED_ARM64_MANIFEST_DIGEST"
echo "Pushed arm64 manifest digest (local): $PUSHED_ARM64_DIGEST"
echo "Pushed arm64 manifest digest (registry): $PUSHED_ARM64_REGISTRY_DIGEST"
echo "Final arm64 digest: $FINAL_ARM64_SHA"
if [ "$FINAL_AMD64_SHA" != "${{ steps.source-manifest.outputs.amd64_sha }}" ] || [ "$FINAL_ARM64_SHA" != "${{ steps.source-manifest.outputs.arm64_sha }}" ]; then
echo "Error: Final manifest SHAs do not match source SHAs"
exit 1
fi
echo "Successfully created multi-arch ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest from ${{ github.event.inputs.tag }}"

273
.github/workflows/e2e_parameterized.yml vendored Normal file
View File

@ -0,0 +1,273 @@
name: 'Parameterized e2e tests'
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch name or Pull Request number (e.g., master or 6102)'
required: true
default: 'master'
type: string
mempool_hostname:
description: 'Mempool Hostname'
required: true
default: 'mempool.space'
type: string
liquid_hostname:
description: 'Liquid Hostname'
required: true
default: 'liquid.network'
type: string
jobs:
cache:
name: "Cache assets for builds"
runs-on: ubuntu-latest
steps:
- name: Determine checkout ref
id: determine-ref
run: |
REF_INPUT="${{ github.event.inputs.ref }}"
if [[ "$REF_INPUT" =~ ^[0-9]+$ ]]; then
echo "ref=refs/pull/$REF_INPUT/head" >> $GITHUB_OUTPUT
else
echo "ref=$REF_INPUT" >> $GITHUB_OUTPUT
fi
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ steps.determine-ref.outputs.ref }}
path: assets
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 22.14.0
registry-url: "https://registry.npmjs.org"
- name: Install (Prod dependencies only)
run: npm ci --omit=dev --omit=optional
working-directory: assets/frontend
- name: Restore cached mining pool assets
continue-on-error: true
id: cache-mining-pool-restore
uses: actions/cache/restore@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Restore promo video assets
continue-on-error: true
id: cache-promo-video-restore
uses: actions/cache/restore@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
- name: Unzip assets before building (src/resources)
continue-on-error: true
run: unzip -o mining-pool-assets.zip -d assets/frontend/src/resources/mining-pools
- name: Unzip assets before building (src/resources)
continue-on-error: true
run: unzip -o promo-video-assets.zip -d assets/frontend/src/resources/promo-video
# - name: Unzip assets before building (dist)
# continue-on-error: true
# run: unzip assets.zip -d assets/frontend/dist/mempool/browser/resources
- name: Sync-assets
run: npm run sync-assets-dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MEMPOOL_CDN: 1
VERBOSE: 1
working-directory: assets/frontend
- name: Zip mining-pool assets
run: zip -jrq mining-pool-assets.zip assets/frontend/src/resources/mining-pools/*
- name: Zip promo-video assets
run: zip -jrq promo-video-assets.zip assets/frontend/src/resources/promo-video/*
- name: Upload mining pool assets as artifact
uses: actions/upload-artifact@v4
with:
name: mining-pool-assets
path: mining-pool-assets.zip
- name: Upload promo video assets as artifact
uses: actions/upload-artifact@v4
with:
name: promo-video-assets
path: promo-video-assets.zip
- name: Save mining pool assets cache
id: cache-mining-pool-save
uses: actions/cache/save@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Save promo video assets cache
id: cache-promo-video-save
uses: actions/cache/save@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
e2e:
runs-on: ubuntu-latest
needs: cache
strategy:
fail-fast: false
matrix:
module: ["mempool", "liquid", "testnet4"]
name: E2E tests for ${{ matrix.module }}
steps:
- name: Determine checkout ref
id: determine-ref
run: |
REF_INPUT="${{ github.event.inputs.ref }}"
if [[ "$REF_INPUT" =~ ^[0-9]+$ ]]; then
echo "ref=refs/pull/$REF_INPUT/head" >> $GITHUB_OUTPUT
else
echo "ref=$REF_INPUT" >> $GITHUB_OUTPUT
fi
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ steps.determine-ref.outputs.ref }}
path: ${{ matrix.module }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 22.14.0
cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
- name: Restore cached mining pool assets
continue-on-error: true
id: cache-mining-pool-restore
uses: actions/cache/restore@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Restore cached promo video assets
continue-on-error: true
id: cache-promo-video-restore
uses: actions/cache/restore@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: mining-pool-assets
- name: Unzip assets before building (src/resources)
run: unzip -o mining-pool-assets.zip -d ${{ matrix.module }}/frontend/src/resources/mining-pools
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: promo-video-assets
- name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }}
LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# liquid
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'liquid' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }}
LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# testnet
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'testnet4' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:mempool
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/testnet4/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }}
LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}

View File

@ -4,7 +4,7 @@ on: [workflow_dispatch]
jobs:
print-backend-sha:
runs-on: 'ubuntu-latest'
runs-on: ubuntu-latest
name: Get block height
steps:
- name: Checkout

View File

@ -4,7 +4,7 @@ on: [workflow_dispatch]
jobs:
print-backend-sha:
runs-on: 'ubuntu-latest'
runs-on: ubuntu-latest
name: Print backend hashes
steps:
- name: Checkout

View File

@ -10,7 +10,7 @@ on:
type: string
jobs:
print-images-sha:
runs-on: 'ubuntu-latest'
runs-on: ubuntu-latest
name: Print digest for images
steps:
- name: Checkout

View File

@ -2,7 +2,7 @@ name: Docker build on tag
env:
DOCKER_CLI_EXPERIMENTAL: enabled
TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$"
DOCKER_BUILDKIT: 0
DOCKER_BUILDKIT: 1 # Enable BuildKit for better performance
COMPOSE_DOCKER_CLI_BUILD: 0
on:
@ -25,13 +25,12 @@ jobs:
timeout-minutes: 120
name: Build and push to DockerHub
steps:
# Workaround based on JonasAlfredsson/docker-on-tmpfs@v1.0.1
- name: Replace the current swap file
shell: bash
run: |
sudo swapoff /mnt/swapfile
sudo rm -v /mnt/swapfile
sudo fallocate -l 13G /mnt/swapfile
sudo swapoff /mnt/swapfile || true
sudo rm -f /mnt/swapfile
sudo fallocate -l 16G /mnt/swapfile
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
@ -50,7 +49,7 @@ jobs:
echo "Directory '/var/lib/docker' not found"
exit 1
fi
sudo mount -t tmpfs -o size=10G tmpfs /var/lib/docker
sudo mount -t tmpfs -o size=12G tmpfs /var/lib/docker
sudo systemctl restart docker
sudo df -h | grep docker
@ -75,10 +74,16 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/amd64,linux/arm64
id: qemu
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
driver-opts: |
network=host
id: buildx
- name: Available platforms
@ -89,19 +94,19 @@ jobs:
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
key: ${{ runner.os }}-buildx-${{ matrix.service }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
${{ runner.os }}-buildx-${{ matrix.service }}-
- name: Run Docker buildx for ${{ matrix.service }} against tag
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
--build-context rustgbt=./rust \
--build-context backend=./backend \
--output "type=registry" ./${{ matrix.service }}/ \
--build-arg commitHash=$SHORT_SHA
--output "type=registry,push=true" \
--build-arg commitHash=$SHORT_SHA \
./${{ matrix.service }}/

2
.nvmrc
View File

@ -1 +1 @@
v20.8.0
v22

View File

@ -10,7 +10,7 @@ However, this copyright license does not include an implied right or license
to use any trademarks, service marks, logos, or trade names of Mempool Space K.K.
or any other contributor to The Mempool Open Source Project.
The Mempool Open Source Project®, Mempool Accelerator, Mempool Enterprise®,
The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®,
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
the mempool block visualization Logo, the mempool Blocks Logo, the mempool

View File

@ -1,23 +1,23 @@
{
"name": "mempool-backend",
"version": "3.1.0-dev",
"version": "3.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-backend",
"version": "3.1.0-dev",
"version": "3.2.0",
"hasInstallScript": true,
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
"axios": "1.8.1",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.21.1",
"maxmind": "~4.3.11",
"mysql2": "~3.12.0",
"mysql2": "~3.13.0",
"redis": "^4.7.0",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
@ -2275,9 +2275,9 @@
}
},
"node_modules/axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@ -6173,9 +6173,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz",
"integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
@ -9459,9 +9459,9 @@
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
},
"axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@ -12337,9 +12337,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz",
"integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==",
"requires": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",

View File

@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "3.1.0-dev",
"version": "3.2.0",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@ -41,12 +41,12 @@
"dependencies": {
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
"axios": "1.8.1",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.21.1",
"maxmind": "~4.3.11",
"mysql2": "~3.12.0",
"mysql2": "~3.13.0",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.7.0",
"socks-proxy-agent": "~7.0.0",

View File

@ -55,6 +55,8 @@ class BitcoinRoutes {
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
.post(config.MEMPOOL.API_URL_PREFIX + 'prevouts', this.$getPrevouts)
.post(config.MEMPOOL.API_URL_PREFIX + 'cpfp', this.getCpfpLocalTxs)
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
// Internal routes
@ -404,8 +406,8 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block);
} catch (e) {
handleError(req, res, 500, 'Failed to get block');
} catch (e: any) {
handleError(req, res, e?.response?.status === 404 ? 404 : 500, 'Failed to get block');
}
}
@ -981,6 +983,92 @@ class BitcoinRoutes {
}
}
private async $getPrevouts(req: Request, res: Response) {
try {
const outpoints = req.body;
if (!Array.isArray(outpoints) || outpoints.some((item) => !/^[a-fA-F0-9]{64}$/.test(item.txid) || typeof item.vout !== 'number')) {
handleError(req, res, 400, 'Invalid outpoints format');
return;
}
if (outpoints.length > 100) {
handleError(req, res, 400, 'Too many outpoints requested');
return;
}
const result = Array(outpoints.length).fill(null);
const memPool = mempool.getMempool();
for (let i = 0; i < outpoints.length; i++) {
const outpoint = outpoints[i];
let prevout: IEsploraApi.Vout | null = null;
let unconfirmed: boolean | null = null;
const mempoolTx = memPool[outpoint.txid];
if (mempoolTx) {
if (outpoint.vout < mempoolTx.vout.length) {
prevout = mempoolTx.vout[outpoint.vout];
unconfirmed = true;
}
} else {
try {
const rawPrevout = await bitcoinClient.getTxOut(outpoint.txid, outpoint.vout, false);
if (rawPrevout) {
prevout = {
value: Math.round(rawPrevout.value * 100000000),
scriptpubkey: rawPrevout.scriptPubKey.hex,
scriptpubkey_asm: rawPrevout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(rawPrevout.scriptPubKey.hex) : '',
scriptpubkey_type: transactionUtils.translateScriptPubKeyType(rawPrevout.scriptPubKey.type),
scriptpubkey_address: rawPrevout.scriptPubKey && rawPrevout.scriptPubKey.address ? rawPrevout.scriptPubKey.address : '',
};
unconfirmed = false;
}
} catch (e) {
// Ignore bitcoin client errors, just leave prevout as null
}
}
if (prevout) {
result[i] = { prevout, unconfirmed };
}
}
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get prevouts');
}
}
private getCpfpLocalTxs(req: Request, res: Response) {
try {
const transactions = req.body;
if (!Array.isArray(transactions) || transactions.some(tx =>
!tx || typeof tx !== 'object' ||
!/^[a-fA-F0-9]{64}$/.test(tx.txid) ||
typeof tx.weight !== 'number' ||
typeof tx.sigops !== 'number' ||
typeof tx.fee !== 'number' ||
!Array.isArray(tx.vin) ||
!Array.isArray(tx.vout)
)) {
handleError(req, res, 400, 'Invalid transactions format');
return;
}
if (transactions.length > 1) {
handleError(req, res, 400, 'More than one transaction is not supported yet');
return;
}
const cpfpInfo = calculateMempoolTxCpfp(transactions[0], mempool.getMempool(), true);
res.json([cpfpInfo]);
} catch (e) {
handleError(req, res, 500, 'Failed to calculate CPFP info');
}
}
}
export default new BitcoinRoutes();

View File

@ -36,7 +36,7 @@ class FailoverRouter {
maxHeight: number = 0;
hosts: FailoverHost[];
multihost: boolean;
gitHashInterval: number = 600000; // 10 minutes
gitHashInterval: number = 60000; // 1 minute
pollInterval: number = 60000; // 1 minute
pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create();
@ -111,7 +111,7 @@ class FailoverRouter {
for (const host of this.hosts) {
try {
const result = await (host.socket
? this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT })
? this.pollConnection.get<number>('http://api/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT })
: this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT })
);
if (result) {
@ -288,7 +288,7 @@ class FailoverRouter {
let url;
if (host.socket) {
axiosConfig = { socketPath: host.host, timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
url = path;
url = 'http://api' + path;
} else {
axiosConfig = { timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
url = host.host + path;

View File

@ -1391,7 +1391,7 @@ class Blocks {
}
public async $getBlockAuditSummary(hash: string): Promise<BlockAudit | null> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && Common.auditIndexingEnabled()) {
return BlocksAuditsRepository.$getBlockAudit(hash);
} else {
return null;
@ -1399,7 +1399,7 @@ class Blocks {
}
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && Common.auditIndexingEnabled()) {
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
} else {
return null;
@ -1469,11 +1469,11 @@ class Blocks {
if (rows && Array.isArray(rows)) {
return rows.map(r => r.definition_hash);
} else {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (no result)`);
logger.debug(`Unable to retrieve list of blocks.definition_hash from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`);
logger.debug(`Unable to retrieve list of blocks.definition_hash from db (exception: ${e})`);
return null;
}
}
@ -1484,11 +1484,11 @@ class Blocks {
if (rows && Array.isArray(rows)) {
return rows.map(r => r.hash);
} else {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (no result)`);
logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
return null;
}
}

View File

@ -739,6 +739,13 @@ export class Common {
);
}
static auditIndexingEnabled(): boolean {
return (
Common.indexingEnabled() &&
config.MEMPOOL.AUDIT === true
);
}
static gogglesIndexingEnabled(): boolean {
return (
Common.blocksSummariesIndexingEnabled() &&

View File

@ -167,8 +167,10 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
/**
* Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for
* that transaction (and all others in the same cluster)
* If the passed transaction is not guaranteed to be in the mempool, set localTx to true: this will
* prevent updating the CPFP data of other transactions in the cluster
*/
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }, localTx: boolean = false): CpfpInfo {
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
tx.cpfpDirty = false;
return {
@ -198,17 +200,26 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool:
totalFee += tx.fees.base;
}
const effectiveFeePerVsize = totalFee / totalVsize;
for (const tx of cluster.values()) {
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].bestDescendant = null;
mempool[tx.txid].cpfpChecked = true;
mempool[tx.txid].cpfpDirty = true;
mempool[tx.txid].cpfpUpdated = Date.now();
}
tx = mempool[tx.txid];
if (localTx) {
tx.effectiveFeePerVsize = effectiveFeePerVsize;
tx.ancestors = Array.from(cluster.get(tx.txid)?.ancestors.values() || []).map(ancestor => ({ txid: ancestor.txid, weight: ancestor.weight, fee: ancestor.fees.base }));
tx.descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !cluster.get(tx.txid)?.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
tx.bestDescendant = null;
} else {
for (const tx of cluster.values()) {
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].bestDescendant = null;
mempool[tx.txid].cpfpChecked = true;
mempool[tx.txid].cpfpDirty = true;
mempool[tx.txid].cpfpUpdated = Date.now();
}
tx = mempool[tx.txid];
}
return {
ancestors: tx.ancestors || [],

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 95;
private static currentVersion = 96;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@ -1130,6 +1130,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)');
await this.updateToSchemaVersion(95);
}
if (databaseSchemaVersion < 96) {
await this.$executeQuery(`ALTER TABLE blocks_audits MODIFY time timestamp NOT NULL DEFAULT 0`);
await this.updateToSchemaVersion(96);
}
}
/**

View File

@ -8,6 +8,7 @@ import mining from './mining/mining';
import transactionUtils from './transaction-utils';
import BlocksRepository from '../repositories/BlocksRepository';
import redisCache from './redis-cache';
import blocks from './blocks';
class PoolsParser {
miningPools: any[] = [];
@ -42,6 +43,8 @@ class PoolsParser {
await this.$insertUnknownPool();
let reindexUnknown = false;
let clearCache = false;
for (const pool of this.miningPools) {
if (!pool.id) {
@ -78,17 +81,20 @@ class PoolsParser {
logger.debug(`Inserting new mining pool ${pool.name}`);
await PoolsRepository.$insertNewMiningPool(pool, slug);
reindexUnknown = true;
clearCache = true;
} else {
if (poolDB.name !== pool.name) {
// Pool has been renamed
const newSlug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase();
logger.warn(`Renaming ${poolDB.name} mining pool to ${pool.name}. Slug has been updated. Maybe you want to make a redirection from 'https://mempool.space/mining/pool/${poolDB.slug}' to 'https://mempool.space/mining/pool/${newSlug}`);
await PoolsRepository.$renameMiningPool(poolDB.id, newSlug, pool.name);
clearCache = true;
}
if (poolDB.link !== pool.link) {
// Pool link has changed
logger.debug(`Updating link for ${pool.name} mining pool`);
await PoolsRepository.$updateMiningPoolLink(poolDB.id, pool.link);
clearCache = true;
}
if (JSON.stringify(pool.addresses) !== poolDB.addresses ||
JSON.stringify(pool.regexes) !== poolDB.regexes) {
@ -96,6 +102,7 @@ class PoolsParser {
logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`);
await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes);
reindexUnknown = true;
clearCache = true;
await this.$reindexBlocksForPool(poolDB.id);
}
}
@ -111,6 +118,19 @@ class PoolsParser {
}
await this.$reindexBlocksForPool(unknownPool.id);
}
// refresh the in-memory block cache with the reindexed data
if (clearCache) {
for (const block of blocks.getBlocks()) {
const reindexedBlock = await blocks.$indexBlock(block.height);
if (reindexedBlock.id === block.id) {
block.extras.pool = reindexedBlock.extras.pool;
}
}
// update persistent cache with the reindexed data
diskCache.$saveCacheToDisk();
redisCache.$updateBlocks(blocks.getBlocks());
}
}
public matchBlockMiner(scriptsig: string, addresses: string[], pools: PoolTag[]): PoolTag | undefined {

View File

@ -30,6 +30,7 @@ const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
class WalletApi {
private wallets: Record<string, Wallet> = {};
private syncing = false;
private lastSync = 0;
constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
@ -47,7 +48,38 @@ class WalletApi {
if (!config.WALLETS.ENABLED || this.syncing) {
return;
}
this.syncing = true;
if (config.WALLETS.AUTO && (Date.now() - this.lastSync) > POLL_FREQUENCY) {
try {
// update list of active wallets
this.lastSync = Date.now();
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets`);
const walletList: string[] = response.data;
if (walletList) {
// create a quick lookup dictionary of active wallets
const newWallets: Record<string, boolean> = Object.fromEntries(
walletList.map(wallet => [wallet, true])
);
for (const wallet of walletList) {
// don't overwrite existing wallets
if (!this.wallets[wallet]) {
this.wallets[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
}
}
// remove wallets that are no longer active
for (const wallet of Object.keys(this.wallets)) {
if (!newWallets[wallet]) {
delete this.wallets[wallet];
}
}
}
} catch (e) {
logger.err(`Error updating active wallets: ${(e instanceof Error ? e.message : e)}`);
}
}
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
@ -72,6 +104,7 @@ class WalletApi {
}
}
}
this.syncing = false;
}

View File

@ -420,6 +420,29 @@ class TransactionUtils {
return { prioritized, deprioritized };
}
// Copied from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/bitcoin/bitcoin-api.ts#L324
public translateScriptPubKeyType(outputType: string): string {
const map = {
'pubkey': 'p2pk',
'pubkeyhash': 'p2pkh',
'scripthash': 'p2sh',
'witness_v0_keyhash': 'v0_p2wpkh',
'witness_v0_scripthash': 'v0_p2wsh',
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return'
};
if (map[outputType]) {
return map[outputType];
} else {
return 'unknown';
}
}
}
export default new TransactionUtils();

View File

@ -1011,15 +1011,19 @@ class WebsocketHandler {
const blockTransactions = structuredClone(transactions);
this.printLogs();
await statistics.runStatistics();
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
await statistics.runStatistics();
}
const _memPool = memPool.getMempool();
const candidateTxs = await memPool.getMempoolCandidates();
let candidates: GbtCandidates | undefined = (memPool.limitGBT && candidateTxs) ? { txs: candidateTxs, added: [], removed: [] } : undefined;
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
const accelerations = Object.values(mempool.getAccelerations());
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
if (config.DATABASE.ENABLED) {
const accelerations = Object.values(mempool.getAccelerations());
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
}
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleRbfTransactions(rbfTransactions);
@ -1095,7 +1099,9 @@ class WebsocketHandler {
if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) {
const firstSeen = getRecentFirstSeen(block.id);
if (firstSeen) {
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
if (config.DATABASE.ENABLED) {
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
}
block.extras.firstSeen = firstSeen;
}
}
@ -1392,7 +1398,9 @@ class WebsocketHandler {
});
}
await statistics.runStatistics();
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
await statistics.runStatistics();
}
}
public handleNewStratumJob(job: StratumJob): void {

View File

@ -164,6 +164,7 @@ interface IConfig {
},
WALLETS: {
ENABLED: boolean;
AUTO: boolean;
WALLETS: string[];
},
STRATUM: {
@ -334,6 +335,7 @@ const defaults: IConfig = {
},
'WALLETS': {
'ENABLED': false,
'AUTO': false,
'WALLETS': [],
},
'STRATUM': {

View File

@ -131,6 +131,9 @@ class Server {
this.app
.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With');
res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count,X-Mempool-Auth');
next();
})
.use(express.urlencoded({ extended: true }))
@ -153,7 +156,9 @@ class Server {
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$();
await mempoolBlocks.updatePools$();
if (config.DATABASE.ENABLED) {
await mempoolBlocks.updatePools$();
}
if (config.MEMPOOL.ENABLED) {
if (config.MEMPOOL.CACHE_ENABLED) {
await diskCache.$loadMempoolCache();

View File

@ -93,7 +93,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query);
return rows.map(row => row.timestamp);
} catch (e) {
logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
logger.err('Cannot retrieve indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e;
}
}

View File

@ -98,7 +98,8 @@ class PoolsUpdater {
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
} catch (e) {
this.lastRun = now - 600; // Try again in 10 minutes
// fast-forward lastRun to 10 minutes before the next scheduled update
this.lastRun = now - Math.max(config.MEMPOOL.POOLS_UPDATE_DELAY - 600, 600);
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag);
}
}

View File

@ -1,20 +1,20 @@
FROM node:20.15.0-buster-slim AS builder
FROM rust:1.84-bookworm AS builder
ARG commitHash
ENV MEMPOOL_COMMIT_HASH=${commitHash}
WORKDIR /build
RUN apt-get update && \
apt-get install -y curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y nodejs=22.14.0-1nodesource1 build-essential python3 pkg-config && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY . .
RUN apt-get update
RUN apt-get install -y build-essential python3 pkg-config curl ca-certificates
# Install Rust via rustup
RUN CPU_ARCH=$(uname -m); if [ "$CPU_ARCH" = "armv7l" ]; then c_rehash; fi
#RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
#Workaround to run on github actions from https://github.com/rust-lang/rustup/issues/2700#issuecomment-1367488985
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sed 's#/proc/self/exe#\/bin\/sh#g' | sh -s -- -y --default-toolchain stable
ENV PATH="/root/.cargo/bin:$PATH"
ENV PATH="/usr/local/cargo/bin:$PATH"
COPY --from=backend . .
COPY --from=rustgbt . ../rust/
@ -24,7 +24,14 @@ RUN npm install --omit=dev --omit=optional
WORKDIR /build
RUN npm run package
FROM node:20.15.0-buster-slim
FROM rust:1.84-bookworm AS runtime
RUN apt-get update && \
apt-get install -y curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y nodejs=22.14.0-1nodesource1 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /backend

View File

@ -26,7 +26,7 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=true}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
__MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}

View File

@ -1,4 +1,4 @@
FROM node:20.15.0-buster-slim AS builder
FROM node:22.14.0-bookworm-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}

View File

@ -261,20 +261,14 @@
"proxyConfig": "proxy.conf.mixed.js",
"verbose": true
},
"staging": {
"proxyConfig": "proxy.conf.js",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": true
},
"local-prod": {
"proxyConfig": "proxy.conf.js",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": false
},
"local-staging": {
"proxyConfig": "proxy.conf.staging.js",
"parameterized": {
"proxyConfig": "proxy.conf.parameterized.js",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": false
@ -371,4 +365,4 @@
}
}
}
}
}

View File

@ -16,10 +16,10 @@
"mobileOrder": 4
},
{
"component": "balance",
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
"wallet": "ONBTC"
}
},
{
@ -30,21 +30,22 @@
}
},
{
"component": "address",
"component": "wallet",
"mobileOrder": 2,
"props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
"period": "1m"
"wallet": "ONBTC",
"period": "1m",
"label": "bitcoin.gob.sv"
}
},
{
"component": "blocks"
},
{
"component": "addressTransactions",
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
"wallet": "ONBTC"
}
}
]

View File

@ -57,11 +57,6 @@ describe('Liquid', () => {
});
});
it('loads the tv page - desktop', () => {
cy.visit(`${basePath}/tv`);
cy.waitForSkeletonGone();
});
it('loads the graphs page - mobile', () => {
cy.visit(`${basePath}`)
cy.waitForSkeletonGone();

View File

@ -57,11 +57,6 @@ describe('Liquid Testnet', () => {
cy.waitForSkeletonGone();
});
it('loads the tv page - desktop', () => {
cy.visit(`${basePath}/tv`);
cy.waitForSkeletonGone();
});
it('loads the graphs page - mobile', () => {
cy.visit(`${basePath}`)
cy.waitForSkeletonGone();

View File

@ -1,4 +1,4 @@
import { emitMempoolInfo, dropWebSocket } from '../../support/websocket';
import { emitMempoolInfo, dropWebSocket, receiveWebSocketMessageFromServer } from '../../support/websocket';
const baseModule = Cypress.env('BASE_MODULE');
@ -216,6 +216,69 @@ describe('Mainnet', () => {
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').its('length').should('equal', 2);
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').invoke('text').should('contain', `${address}`);
});
describe('address poisoning', () => {
it('highlights potential address poisoning attacks on outputs, prefix and infix', () => {
const txid = '152a5dea805f95d6f83e50a9fd082630f542a52a076ebabdb295723eaf53fa30';
const prefix = '1DonatePLease';
const infix1 = 'SenderAddressXVXCmAY';
const infix2 = '5btcToSenderXXWBoKhB';
cy.visit(`/tx/${txid}`);
cy.waitForSkeletonGone();
cy.get('.alert-mempool').should('exist');
cy.get('.poison-alert').its('length').should('equal', 2);
cy.get('.prefix')
.should('have.length', 2)
.each(($el) => {
cy.wrap($el).should('have.text', prefix);
});
cy.get('.infix')
.should('have.length', 2)
.then(($infixes) => {
cy.wrap($infixes[0]).should('have.text', infix1);
cy.wrap($infixes[1]).should('have.text', infix2);
});
});
it('highlights potential address poisoning attacks on inputs and outputs, prefix, infix and postfix', () => {
const txid = '44544516084ea916ff1eb69c675c693e252addbbaf77102ffff86e3979ac6132';
const prefix = 'bc1qge8';
const infix1 = '6gqjnk8aqs3nvv7ejrvcd4zq6qur3';
const infix2 = 'xyxjex6zzzx5g8hh65vsel4e548p2';
const postfix1 = '6p6e3r';
const postfix2 = '6p6e3r';
cy.visit(`/tx/${txid}`);
cy.waitForSkeletonGone();
cy.get('.alert-mempool').should('exist');
cy.get('.poison-alert').its('length').should('equal', 2);
cy.get('.prefix')
.should('have.length', 2)
.each(($el) => {
cy.wrap($el).should('have.text', prefix);
});
cy.get('.infix')
.should('have.length', 2)
.then(($infixes) => {
cy.wrap($infixes[0]).should('have.text', infix1);
cy.wrap($infixes[1]).should('have.text', infix2);
});
cy.get('.postfix')
.should('have.length', 2)
.then(($postfixes) => {
cy.wrap($postfixes[0]).should('include.text', postfix1);
cy.wrap($postfixes[1]).should('include.text', postfix2);
});
});
});
});
describe('blocks navigation', () => {
@ -397,6 +460,7 @@ describe('Mainnet', () => {
cy.get('#dropdownFees').should('be.visible');
cy.get('.btn-group').should('be.visible');
});
it('check buttons - tablet', () => {
cy.viewport('ipad-2');
cy.visit('/graphs');
@ -405,6 +469,7 @@ describe('Mainnet', () => {
cy.get('#dropdownFees').should('be.visible');
cy.get('.btn-group').should('be.visible');
});
it('check buttons - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/graphs');
@ -415,26 +480,6 @@ describe('Mainnet', () => {
});
});
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/graphs/mempool');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('macbook-16');
cy.get('.chart-holder');
cy.get('.blockchain-wrapper').should('be.visible');
cy.get('#mempool-block-0').should('be.visible');
});
});
it('loads the tv screen - mobile', () => {
cy.viewport('iphone-6');
cy.visit('/tv');
cy.waitForSkeletonGone();
cy.get('.chart-holder');
cy.get('.blockchain-wrapper').should('not.visible');
});
it('loads the api screen', () => {
cy.visit('/');
cy.waitForSkeletonGone();
@ -516,7 +561,44 @@ describe('Mainnet', () => {
});
describe('RBF transactions', () => {
it('shows RBF transactions properly (mobile)', () => {
it('RBF page gets updated over websockets', () => {
cy.intercept('/api/v1/replacements', {
statusCode: 200,
body: []
});
cy.intercept('/api/v1/fullrbf/replacements', {
statusCode: 200,
body: []
});
cy.mockMempoolSocketV2();
cy.visit('/rbf');
cy.get('.no-replacements');
cy.get('.tree').should('have.length', 0);
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'rbf_page/rbf_01.json'
}
}
});
cy.get('.tree').should('have.length', 1);
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'rbf_page/rbf_02.json'
}
}
});
cy.get('.tree').should('have.length', 2);
});
it('shows RBF transactions properly (mobile - details)', () => {
cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', {
fixture: 'mainnet_tx_cached.json'
}).as('cached_tx');
@ -527,7 +609,7 @@ describe('Mainnet', () => {
cy.viewport('iphone-xr');
cy.mockMempoolSocket();
cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f');
cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f?mode=details');
cy.waitForSkeletonGone();
@ -545,7 +627,120 @@ describe('Mainnet', () => {
}
});
cy.get('.alert-mempool').should('be.visible');
});
it('shows RBF transactions properly (mobile - tracker)', () => {
cy.mockMempoolSocketV2();
cy.viewport('iphone-xr');
// API Mocks
cy.intercept('/api/v1/mining/pools/1w', {
fixture: 'details_rbf/api_mining_pools_1w.json'
}).as('api_mining_1w');
cy.intercept('/api/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29', {
statusCode: 404,
body: 'Transaction not found'
}).as('api_tx01_404');
cy.intercept('/api/v1/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29/cached', {
fixture: 'details_rbf/tx01_api_cached.json'
}).as('api_tx01_cached');
cy.intercept('/api/v1/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29/rbf', {
fixture: 'details_rbf/tx01_api_rbf.json'
}).as('api_tx01_rbf');
cy.visit('/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29?mode=tracker');
cy.wait('@api_tx01_rbf');
// Start sending mocked WS messages
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx01_ws_stratum_jobs.json'
}
}
});
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx01_ws_blocks_01.json'
}
}
});
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx01_ws_tx_replaced.json'
}
}
});
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx01_ws_mempool_blocks_01.json'
}
}
});
cy.get('.alert-replaced').should('be.visible');
cy.get('.explainer').should('be.visible');
cy.get('svg[data-icon=timeline]').should('be.visible');
// Second TX setup
cy.intercept('/api/tx/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', {
fixture: 'details_rbf/tx02_api_tx.json'
}).as('tx02_api');
cy.intercept('/api/v1/transaction-times?txId%5B%5D=b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', {
fixture: 'details_rbf/tx02_api_tx_times.json'
}).as('tx02_api_tx_times');
cy.intercept('/api/v1/tx/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698/rbf', {
fixture: 'details_rbf/tx02_api_rbf.json'
}).as('tx02_api_rbf');
cy.intercept('/api/v1/cpfp/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', {
fixture: 'details_rbf/tx02_api_cpfp.json'
}).as('tx02_api_cpfp');
// Go to the replacement tx
cy.get('.alert-replaced a').click();
cy.wait('@tx02_api_cpfp');
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx02_ws_tx_position.json'
}
}
});
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx02_ws_mempool_blocks_01.json'
}
}
});
cy.get('svg[data-icon=hourglass-half]').should('be.visible');
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx02_ws_block.json'
}
}
});
cy.get('app-confirmations');
cy.get('svg[data-icon=circle-check]').should('be.visible');
});
it('shows RBF transactions properly (desktop)', () => {

View File

@ -60,30 +60,6 @@ describe('Signet', () => {
});
});
describe.skip('tv mode', () => {
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/signet/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.get('.chart-holder').should('be.visible');
cy.get('#mempool-block-0').should('be.visible');
cy.get('.tv-only').should('not.exist');
});
});
it('loads the tv screen - mobile', () => {
cy.visit('/signet/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('iphone-8');
cy.get('.chart-holder').should('be.visible');
cy.get('.tv-only').should('not.exist');
cy.get('#mempool-block-0').should('be.visible');
});
});
});
it('loads the api screen', () => {
cy.visit('/signet');
cy.waitForSkeletonGone();

View File

@ -60,30 +60,6 @@ describe('Testnet4', () => {
});
});
describe('tv mode', () => {
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/testnet4/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.wait(1000);
cy.get('.tv-only').should('not.exist');
cy.get('#mempool-block-0').should('be.visible');
});
});
it('loads the tv screen - mobile', () => {
cy.visit('/testnet4/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('iphone-6');
cy.wait(1000);
cy.get('.tv-only').should('not.exist');
});
});
});
it('loads the api screen', () => {
cy.visit('/testnet4');
cy.waitForSkeletonGone();

View File

@ -0,0 +1,60 @@
{
"txSummary": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"effectiveVsize": 224,
"effectiveFee": 960,
"ancestorCount": 1
},
"cost": 1000,
"targetFeeRate": 3,
"nextBlockFee": 672,
"userBalance": 0,
"mempoolBaseFee": 50000,
"vsizeFee": 0,
"pools": [
36,
102,
112,
44,
4,
2,
6,
94,
143,
43,
105,
115,
142,
111
],
"options": [
{
"fee": 1000
},
{
"fee": 2000
},
{
"fee": 10000
}
],
"hasAccess": false,
"availablePaymentMethods": {
"bitcoin": {
"enabled": true,
"min": 1000,
"max": 10000000
},
"applePay": {
"enabled": true,
"min": 10,
"max": 1000
},
"googlePay": {
"enabled": true,
"min": 10,
"max": 1000
}
},
"unavailable": false
}

View File

@ -0,0 +1,3 @@
{
"gitCommit": "62f80296"
}

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,260 @@
{
"pools": [
{
"poolId": 112,
"name": "Foundry USA",
"link": "https://foundrydigital.com",
"blockCount": 323,
"rank": 1,
"emptyBlocks": 0,
"slug": "foundryusa",
"avgMatchRate": 99.96,
"avgFeeDelta": "-0.01971455",
"poolUniqueId": 111
},
{
"poolId": 45,
"name": "AntPool",
"link": "https://www.antpool.com",
"blockCount": 171,
"rank": 2,
"emptyBlocks": 0,
"slug": "antpool",
"avgMatchRate": 99.99,
"avgFeeDelta": "-0.04227368",
"poolUniqueId": 44
},
{
"poolId": 74,
"name": "ViaBTC",
"link": "https://viabtc.com",
"blockCount": 166,
"rank": 3,
"emptyBlocks": 0,
"slug": "viabtc",
"avgMatchRate": 99.99,
"avgFeeDelta": "-0.02530964",
"poolUniqueId": 73
},
{
"poolId": 37,
"name": "F2Pool",
"link": "https://www.f2pool.com",
"blockCount": 104,
"rank": 4,
"emptyBlocks": 0,
"slug": "f2pool",
"avgMatchRate": 99.99,
"avgFeeDelta": "-0.03299327",
"poolUniqueId": 36
},
{
"poolId": 116,
"name": "MARA Pool",
"link": "https://marapool.com",
"blockCount": 66,
"rank": 5,
"emptyBlocks": 0,
"slug": "marapool",
"avgMatchRate": 99.97,
"avgFeeDelta": "0.02366061",
"poolUniqueId": 115
},
{
"poolId": 103,
"name": "SpiderPool",
"link": "https://www.spiderpool.com",
"blockCount": 46,
"rank": 6,
"emptyBlocks": 1,
"slug": "spiderpool",
"avgMatchRate": 97.82,
"avgFeeDelta": "-0.07258913",
"poolUniqueId": 102
},
{
"poolId": 142,
"name": "SECPOOL",
"link": "https://www.secpool.com",
"blockCount": 30,
"rank": 7,
"emptyBlocks": 1,
"slug": "secpool",
"avgMatchRate": 96.67,
"avgFeeDelta": "-0.06596000",
"poolUniqueId": 141
},
{
"poolId": 106,
"name": "Binance Pool",
"link": "https://pool.binance.com",
"blockCount": 28,
"rank": 8,
"emptyBlocks": 0,
"slug": "binancepool",
"avgMatchRate": 99.99,
"avgFeeDelta": "-0.05834286",
"poolUniqueId": 105
},
{
"poolId": 5,
"name": "Luxor",
"link": "https://mining.luxor.tech",
"blockCount": 28,
"rank": 9,
"emptyBlocks": 0,
"slug": "luxor",
"avgMatchRate": 100,
"avgFeeDelta": "-0.05496071",
"poolUniqueId": 4
},
{
"poolId": 143,
"name": "OCEAN",
"link": "https://ocean.xyz/",
"blockCount": 12,
"rank": 10,
"emptyBlocks": 0,
"slug": "ocean",
"avgMatchRate": 91.9,
"avgFeeDelta": "-0.14650833",
"poolUniqueId": 142
},
{
"poolId": 44,
"name": "Braiins Pool",
"link": "https://braiins.com/pool",
"blockCount": 12,
"rank": 11,
"emptyBlocks": 0,
"slug": "braiinspool",
"avgMatchRate": 100,
"avgFeeDelta": "-0.03553333",
"poolUniqueId": 43
},
{
"poolId": 113,
"name": "SBI Crypto",
"link": "https://sbicrypto.com",
"blockCount": 8,
"rank": 12,
"emptyBlocks": 0,
"slug": "sbicrypto",
"avgMatchRate": 98.65,
"avgFeeDelta": "-0.04246250",
"poolUniqueId": 112
},
{
"poolId": 152,
"name": "Carbon Negative",
"link": "https://github.com/bitcoin-data/mining-pools/issues/48",
"blockCount": 7,
"rank": 13,
"emptyBlocks": 0,
"slug": "carbonnegative",
"avgMatchRate": 99.75,
"avgFeeDelta": "-0.04407143",
"poolUniqueId": 151
},
{
"poolId": 7,
"name": "BTC.com",
"link": "https://pool.btc.com",
"blockCount": 5,
"rank": 14,
"emptyBlocks": 0,
"slug": "btccom",
"avgMatchRate": 99.98,
"avgFeeDelta": "-0.02496000",
"poolUniqueId": 6
},
{
"poolId": 162,
"name": "Mining Squared",
"link": "https://pool.bsquared.network/",
"blockCount": 4,
"rank": 15,
"emptyBlocks": 0,
"slug": "miningsquared",
"avgMatchRate": 100,
"avgFeeDelta": "-0.00915000",
"poolUniqueId": 161
},
{
"poolId": 95,
"name": "Poolin",
"link": "https://www.poolin.com",
"blockCount": 4,
"rank": 16,
"emptyBlocks": 0,
"slug": "poolin",
"avgMatchRate": 100,
"avgFeeDelta": "-0.26485000",
"poolUniqueId": 94
},
{
"poolId": 1,
"name": "Unknown",
"link": "https://learnmeabitcoin.com/technical/coinbase-transaction",
"blockCount": 4,
"rank": 17,
"emptyBlocks": 0,
"slug": "unknown",
"avgMatchRate": 100,
"avgFeeDelta": "-0.06490000",
"poolUniqueId": 0
},
{
"poolId": 144,
"name": "WhitePool",
"link": "https://whitebit.com/mining-pool",
"blockCount": 3,
"rank": 18,
"emptyBlocks": 0,
"slug": "whitepool",
"avgMatchRate": 100,
"avgFeeDelta": "-0.01293333",
"poolUniqueId": 143
},
{
"poolId": 3,
"name": "ULTIMUSPOOL",
"link": "https://www.ultimuspool.com",
"blockCount": 1,
"rank": 19,
"emptyBlocks": 0,
"slug": "ultimuspool",
"avgMatchRate": 100,
"avgFeeDelta": "-0.16130000",
"poolUniqueId": 2
},
{
"poolId": 50,
"name": "Solo CK",
"link": "https://solo.ckpool.org",
"blockCount": 1,
"rank": 20,
"emptyBlocks": 0,
"slug": "solock",
"avgMatchRate": 100,
"avgFeeDelta": "-0.01510000",
"poolUniqueId": 49
},
{
"poolId": 158,
"name": "BitFuFuPool",
"link": "https://www.bitfufu.com/pool",
"blockCount": 1,
"rank": 21,
"emptyBlocks": 0,
"slug": "bitfufupool",
"avgMatchRate": 100,
"avgFeeDelta": "-0.01630000",
"poolUniqueId": 157
}
],
"blockCount": 1024,
"lastEstimatedHashrate": 786391245138648900000,
"lastEstimatedHashrate3d": 797683179385121300000,
"lastEstimatedHashrate1w": 827836055441520300000
}

View File

@ -0,0 +1,55 @@
{
"txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "fb16c141d22a3a7af5e44e7478a8663866c35d554cd85107ec8a99b97e5a72e9",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj",
"value": 50000
},
"scriptsig": "483045022100f44a50e894c30b28400933da120b872ff15bd2e66dd034ffbbb925fd054dd60f02206ba477a9da3fae68a9f420f53bc25493270bd987d13b75fef39cf514aea9cdb3014104ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df",
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100f44a50e894c30b28400933da120b872ff15bd2e66dd034ffbbb925fd054dd60f02206ba477a9da3fae68a9f420f53bc25493270bd987d13b75fef39cf514aea9cdb301 OP_PUSHBYTES_65 04ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df",
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "5120a2fc5cb445755389eed295ab9d594b0facd3e00840a3e477fa7c025412c53795",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 a2fc5cb445755389eed295ab9d594b0facd3e00840a3e477fa7c025412c53795",
"scriptpubkey_type": "v1_p2tr",
"scriptpubkey_address": "bc1p5t79edz9w4fcnmkjjk4e6k2tp7kd8cqggz37gal60sp9gyk9x72sk4mk0f",
"value": 49394
}
],
"size": 233,
"weight": 932,
"sigops": 0,
"fee": 606,
"status": {
"confirmed": false
},
"order": 701313494,
"vsize": 233,
"adjustedVsize": 233,
"feePerVsize": 2.6008583690987126,
"adjustedFeePerVsize": 2.6008583690987126,
"effectiveFeePerVsize": 2.6008583690987126,
"firstSeen": 1743541407,
"inputs": [],
"cpfpDirty": false,
"ancestors": [],
"descendants": [],
"bestDescendant": null,
"position": {
"block": 0,
"vsize": 318595.5
},
"flags": 1099511645193
}

View File

@ -0,0 +1,34 @@
{
"replacements": {
"tx": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"fee": 960,
"vsize": 224,
"value": 49040,
"rate": 4.285714285714286,
"time": 1743541726,
"rbf": true,
"fullRbf": false
},
"time": 1743541726,
"fullRbf": false,
"replaces": [
{
"tx": {
"txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29",
"fee": 606,
"vsize": 233,
"value": 49394,
"rate": 2.6008583690987126,
"time": 1743541407,
"rbf": true
},
"time": 1743541407,
"interval": 319,
"fullRbf": false,
"replaces": []
}
]
},
"replaces": null
}

View File

@ -0,0 +1,579 @@
{
"blocks": [
{
"id": "0000000000000000000079f5b74b6533abb0b1ece06570d8d157b5bebd1460b4",
"height": 890440,
"version": 559235072,
"timestamp": 1743535677,
"bits": 386038124,
"nonce": 2920325684,
"difficulty": 113757508810854,
"merkle_root": "c793d5fdbfb1ebe99e14a13a6d65370057d311774d33c71da166663b18722474",
"tx_count": 3823,
"size": 1578209,
"weight": 3993461,
"previousblockhash": "000000000000000000020fb2e24425793e17e60e188205dc1694d221790348b2",
"mediantime": 1743532406,
"stale": false,
"extras": {
"reward": 319838750,
"coinbaseRaw": "0348960d082f5669614254432f2cfabe6d6d294719da11c017243828bf32c405341db7f19387fee92c25413c45e114907f9810000000000000001058bf9601429f9fa7a6c160d10d00000000000000",
"orphans": [],
"medianFee": 4,
"feeRange": [
3,
3,
3.0191082802547773,
3.980952380952381,
5,
10,
427.748502994012
],
"totalFees": 7338750,
"avgFee": 1920,
"avgFeeRate": 7,
"utxoSetChange": 4093,
"avgTxSize": 412.71000000000004,
"totalInputs": 7430,
"totalOutputs": 11523,
"totalOutputAmt": 547553568373,
"segwitTotalTxs": 3432,
"segwitTotalSize": 1467920,
"segwitTotalWeight": 3552413,
"feePercentiles": null,
"virtualSize": 998365.25,
"coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4",
"coinbaseAddresses": [
"1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4"
],
"coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG",
"coinbaseSignatureAscii": "\u0003H–\r\b/ViaBTC/,ú¾mm)G\u0019Ú\u0011À\u0017$8(¿2Ä\u00054\u001d·ñ“‡þé,%A<Eá\u0014˜\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010X¿–\u0001BŸŸ§¦Á`Ñ\r\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
"header": "00405521b248037921d29416dc0582180ee6173e792544e2b20f02000000000000000000742472183b6666a11dc7334d7711d3570037656d3aa1149ee9ebb1bffdd593c73d3eec676c79021734a210ae",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 73,
"name": "ViaBTC",
"slug": "viabtc",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 7342711,
"expectedWeight": 3991920,
"similarity": 0.9978345275528634
}
},
{
"id": "00000000000000000000ef5a27459785ea4c91e05f64adfad306af6dfc0cd19c",
"height": 890441,
"version": 540188672,
"timestamp": 1743535803,
"bits": 386038124,
"nonce": 3418993591,
"difficulty": 113757508810854,
"merkle_root": "25457e2f9e9b55cacde6166acc58ebc2367007a7af5d9b39f46dc1ce060fd63e",
"tx_count": 2813,
"size": 1671284,
"weight": 3993398,
"previousblockhash": "0000000000000000000079f5b74b6533abb0b1ece06570d8d157b5bebd1460b4",
"mediantime": 1743532471,
"stale": false,
"extras": {
"reward": 315271317,
"coinbaseRaw": "0349960d04bb3eec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f01530a4561e7020000000000",
"orphans": [],
"medianFee": 2.0116279069767438,
"feeRange": [
1.0567375886524824,
1.1917098445595855,
1.9007678676904902,
2.1298076923076925,
2.825034262220192,
3.175202156334232,
121
],
"totalFees": 2771317,
"avgFee": 985,
"avgFeeRate": 2,
"utxoSetChange": 37,
"avgTxSize": 593.97,
"totalInputs": 8679,
"totalOutputs": 8716,
"totalOutputAmt": 299780221694,
"segwitTotalTxs": 2379,
"segwitTotalSize": 1535232,
"segwitTotalWeight": 3449298,
"feePercentiles": null,
"virtualSize": 998349.5,
"coinbaseAddress": "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
"coinbaseAddresses": [
"bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38"
],
"coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 0f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a3",
"coinbaseSignatureAscii": "\u0003I–\r\u0004»>ìg/Foundry USA Pool #dropgold/\u0001S\nEaç\u0002\u0000\u0000\u0000\u0000\u0000",
"header": "00a03220b46014bdbeb557d1d87065e0ecb1b0ab33654bb7f579000000000000000000003ed60f06cec16df4399b5dafa7077036c2eb58cc6a16e6cdca559b9e2f7e4525bb3eec676c790217b7b3c9cb",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 111,
"name": "Foundry USA",
"slug": "foundryusa",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 2792968,
"expectedWeight": 3991959,
"similarity": 0.9951416839808291
}
},
{
"id": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce",
"height": 890442,
"version": 557981696,
"timestamp": 1743536834,
"bits": 386038124,
"nonce": 470697326,
"difficulty": 113757508810854,
"merkle_root": "5e92e681c1db2797a5b3e5016729059f8b60a256cafb51d835dac2b3964c0db4",
"tx_count": 3566,
"size": 1628328,
"weight": 3993552,
"previousblockhash": "00000000000000000000ef5a27459785ea4c91e05f64adfad306af6dfc0cd19c",
"mediantime": 1743532867,
"stale": false,
"extras": {
"reward": 318057766,
"coinbaseRaw": "034a960d194d696e656420627920416e74506f6f6c204d000201e15e2989fabe6d6dd599e9dfa40be51f1517c8f512c5c3d51c7656182f1df335d34b98ee02c527db080000000000000000004f92b702000000000000",
"orphans": [],
"medianFee": 3.00860164711668,
"feeRange": [
1.5174418604651163,
2.0140845070422535,
2.492354740061162,
3,
4.020942408376963,
7,
200
],
"totalFees": 5557766,
"avgFee": 1558,
"avgFeeRate": 5,
"utxoSetChange": 1971,
"avgTxSize": 456.48,
"totalInputs": 7938,
"totalOutputs": 9909,
"totalOutputAmt": 900044492230,
"segwitTotalTxs": 3214,
"segwitTotalSize": 1526463,
"segwitTotalWeight": 3586200,
"feePercentiles": null,
"virtualSize": 998388,
"coinbaseAddress": "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z",
"coinbaseAddresses": [
"37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z",
"39C7fxSzEACPjM78Z7xdPxhf7mKxJwvfMJ"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 42402a28dd61f2718a4b27ae72a4791d5bbdade7 OP_EQUAL",
"coinbaseSignatureAscii": "\u0003J–\r\u0019Mined by AntPool M\u0000\u0002\u0001á^)‰ú¾mmՙéߤ\u000bå\u001f\u0015\u0017Èõ\u0012ÅÃÕ\u001cvV\u0018/\u001dó5ÓK˜î\u0002Å'Û\b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000O’·\u0002\u0000\u0000\u0000\u0000\u0000\u0000",
"header": "002042219cd10cfc6daf06d3faad645fe0914cea859745275aef00000000000000000000b40d4c96b3c2da35d851fbca56a2608b9f05296701e5b3a59727dbc181e6925ec242ec676c7902176e450e1c",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 44,
"name": "AntPool",
"slug": "antpool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 5764747,
"expectedWeight": 3991786,
"similarity": 0.9029319155137951
}
},
{
"id": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6",
"height": 890443,
"version": 706666496,
"timestamp": 1743537197,
"bits": 386038124,
"nonce": 321696065,
"difficulty": 113757508810854,
"merkle_root": "3d7574f7eca741fa94b4690868a242e5b286f8a0417ad0275d4ab05893e96350",
"tx_count": 2155,
"size": 1700002,
"weight": 3993715,
"previousblockhash": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce",
"mediantime": 1743533789,
"stale": false,
"extras": {
"reward": 315112344,
"coinbaseRaw": "034b960d21202020204d696e656420627920536563706f6f6c2020202070000b05e388958c01fabe6d6db7ae4bfa7b1294e16e800b4563f1f5ddeb5c0740319eba45600f3f05d2d7272910000000000000000000c2cb7e020000",
"orphans": [],
"medianFee": 1.4360674424569184,
"feeRange": [
1,
1.0135135135135136,
1.09717868338558,
2.142857142857143,
3.009584664536741,
4.831858407079646,
196.07843137254903
],
"totalFees": 2612344,
"avgFee": 1212,
"avgFeeRate": 2,
"utxoSetChange": -2880,
"avgTxSize": 788.64,
"totalInputs": 9773,
"totalOutputs": 6893,
"totalOutputAmt": 264603969671,
"segwitTotalTxs": 1933,
"segwitTotalSize": 1556223,
"segwitTotalWeight": 3418707,
"feePercentiles": null,
"virtualSize": 998428.75,
"coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9",
"coinbaseAddresses": [
"3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9",
"3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL",
"coinbaseSignatureAscii": "\u0003K–\r! Mined by Secpool p\u0000\u000b\u0005㈕Œ\u0001ú¾mm·®Kú{\u0012”án€\u000bEcñõÝë\\\u0007@1žºE`\u000f?\u0005Ò×')\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000ÂË~\u0002\u0000\u0000",
"header": "00e01e2acedf6db0523987887ed8b989d4f58b3d6b878a974548010000000000000000005063e99358b04a5d27d07a41a0f886b2e542a2680869b494fa41a7ecf774753d2d44ec676c79021741b12c13",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 141,
"name": "SECPOOL",
"slug": "secpool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 2623934,
"expectedWeight": 3991917,
"similarity": 0.9951244468050102
}
},
{
"id": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d",
"height": 890444,
"version": 671080448,
"timestamp": 1743539347,
"bits": 386038124,
"nonce": 994357124,
"difficulty": 113757508810854,
"merkle_root": "c891d4bf68e22916274b667eb3287d50da2ddd63f8dad892da045cc2ad4a7b21",
"tx_count": 3797,
"size": 1500309,
"weight": 3993525,
"previousblockhash": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6",
"mediantime": 1743533986,
"stale": false,
"extras": {
"reward": 318708524,
"coinbaseRaw": "034c960d082f5669614254432f2cfabe6d6d45b7fd7ab53a0914da7dcc9d21fe44f0936f5354169a56df9d5139f07afbc2b41000000000000000106fc0eb03f0ac2e851d18d8d9f85ad70000000000",
"orphans": [],
"medianFee": 4.064775540157046,
"feeRange": [
3.014354066985646,
3.18368700265252,
3.602836879432624,
4.231825525040388,
5.581730769230769,
10,
697.7151162790698
],
"totalFees": 6208524,
"avgFee": 1635,
"avgFeeRate": 6,
"utxoSetChange": 5755,
"avgTxSize": 395.02,
"totalInputs": 6681,
"totalOutputs": 12436,
"totalOutputAmt": 835839828101,
"segwitTotalTxs": 3351,
"segwitTotalSize": 1354446,
"segwitTotalWeight": 3410181,
"feePercentiles": null,
"virtualSize": 998381.25,
"coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4",
"coinbaseAddresses": [
"1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4"
],
"coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG",
"coinbaseSignatureAscii": "\u0003L–\r\b/ViaBTC/,ú¾mmE·ýzµ:\t\u0014Ú}̝!þDð“oST\u0016šQ9ðzû´\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010oÀë\u0003ð¬.…\u001d\u0018ØÙøZ×\u0000\u0000\u0000\u0000\u0000",
"header": "00e0ff27f6f596dc1a210647d530ed3b351b5173428370b2086e02000000000000000000217b4aadc25c04da92d8daf863dd2dda507d28b37e664b271629e268bfd491c8934cec676c79021784af443b",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 73,
"name": "ViaBTC",
"slug": "viabtc",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 6253024,
"expectedWeight": 3991868,
"similarity": 0.9862862477811569
}
},
{
"id": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a",
"height": 890445,
"version": 601202688,
"timestamp": 1743539574,
"bits": 386038124,
"nonce": 1647397133,
"difficulty": 113757508810854,
"merkle_root": "61d8294afa8f6bafa4d979a77d187dee5f75a6392f957ea647d96eefbbbc5e9b",
"tx_count": 3579,
"size": 1659862,
"weight": 3993406,
"previousblockhash": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d",
"mediantime": 1743535677,
"stale": false,
"extras": {
"reward": 315617086,
"coinbaseRaw": "034d960d04764dec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f4fac7c451540000000000000",
"orphans": [],
"medianFee": 2.5565329189526835,
"feeRange": [
1.521613832853026,
2,
2.2411347517730498,
3,
3,
3.954954954954955,
162.78343949044586
],
"totalFees": 3117086,
"avgFee": 871,
"avgFeeRate": 3,
"utxoSetChange": 1881,
"avgTxSize": 463.65000000000003,
"totalInputs": 7893,
"totalOutputs": 9774,
"totalOutputAmt": 324878597485,
"segwitTotalTxs": 3189,
"segwitTotalSize": 1538741,
"segwitTotalWeight": 3509030,
"feePercentiles": null,
"virtualSize": 998351.5,
"coinbaseAddress": "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
"coinbaseAddresses": [
"bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38"
],
"coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 0f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a3",
"coinbaseSignatureAscii": "\u0003M–\r\u0004vMìg/Foundry USA Pool #dropgold/O¬|E\u0015@\u0000\u0000\u0000\u0000\u0000\u0000",
"header": "00a0d5230d2965fa5bd3e9406f5d665f975d9fc34eae70c46eb3010000000000000000009b5ebcbbef6ed947a67e952f39a6755fee7d187da779d9a4af6b8ffa4a29d861764dec676c7902170d493162",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 111,
"name": "Foundry USA",
"slug": "foundryusa",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 3145370,
"expectedWeight": 3991903,
"similarity": 0.9903353189076812
}
},
{
"id": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b",
"height": 890446,
"version": 537722880,
"timestamp": 1743541107,
"bits": 386038124,
"nonce": 826569764,
"difficulty": 113757508810854,
"merkle_root": "d9b320d7cb5aace80ca20b934b13b4a272121fbdd59f3aaba690e0326ca2c144",
"tx_count": 3998,
"size": 1541360,
"weight": 3993545,
"previousblockhash": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a",
"mediantime": 1743535803,
"stale": false,
"extras": {
"reward": 317976882,
"coinbaseRaw": "034e960d20202020204d696e656420627920536563706f6f6c2020202070001b04fad5fdfefabe6d6d59dd8ebce6e5aab8fb943bbdcede474b6f2d00a395a717970104a6958c17f1ca100000000000000000008089c9350200",
"orphans": [],
"medianFee": 3.3750830641948864,
"feeRange": [
2.397163120567376,
3,
3,
3.463647199046484,
4.49438202247191,
7.213930348258707,
476.1904761904762
],
"totalFees": 5476882,
"avgFee": 1370,
"avgFeeRate": 5,
"utxoSetChange": 4951,
"avgTxSize": 385.41,
"totalInputs": 7054,
"totalOutputs": 12005,
"totalOutputAmt": 983289729453,
"segwitTotalTxs": 3538,
"segwitTotalSize": 1396505,
"segwitTotalWeight": 3414233,
"feePercentiles": null,
"virtualSize": 998386.25,
"coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9",
"coinbaseAddresses": [
"3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9",
"3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL",
"coinbaseSignatureAscii": "\u0003N–\r Mined by Secpool p\u0000\u001b\u0004úÕýþú¾mmYݎ¼æåª¸û”;½ÎÞGKo-\u0000£•§\u0017—\u0001\u0004¦•Œ\u0017ñÊ\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000€‰É5\u0002\u0000",
"header": "00000d208a33fa2f65c6d3662ac962c9bd595147b940f96520400100000000000000000044c1a26c32e090a6ab3a9fd5bd1f1272a2b4134b930ba20ce8ac5acbd720b3d97353ec676c79021724744431",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 141,
"name": "SECPOOL",
"slug": "secpool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 5601814,
"expectedWeight": 3991928,
"similarity": 0.9537877497871488
}
},
{
"id": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab",
"height": 890447,
"version": 568860672,
"timestamp": 1743541240,
"bits": 386038124,
"nonce": 4008077709,
"difficulty": 113757508810854,
"merkle_root": "8c3b098e4e50b67075a4fc52bf4cd603aaa450c240c18a865c9ddc0f27104f5f",
"tx_count": 1919,
"size": 1747789,
"weight": 3993172,
"previousblockhash": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b",
"mediantime": 1743536834,
"stale": false,
"extras": {
"reward": 314435106,
"coinbaseRaw": "034f960d0f2f736c7573682f65000002fba05ef1fabe6d6df8d29032ea6f9ab1debd223651f30887df779c6195869e70a7b787b3a15f4b1710000000000000000000ee5f0c00b20200000000",
"orphans": [],
"medianFee": 1.4653828213500366,
"feeRange": [
1.0845070422535212,
1.2,
1.51,
2.0141129032258065,
2.3893805309734515,
4.025477707006369,
300.0065359477124
],
"totalFees": 1935106,
"avgFee": 1008,
"avgFeeRate": 1,
"utxoSetChange": -4244,
"avgTxSize": 910.58,
"totalInputs": 9909,
"totalOutputs": 5665,
"totalOutputAmt": 210763861504,
"segwitTotalTxs": 1720,
"segwitTotalSize": 1629450,
"segwitTotalWeight": 3519924,
"feePercentiles": null,
"virtualSize": 998293,
"coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11",
"coinbaseAddresses": [
"34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL",
"coinbaseSignatureAscii": "\u0003O–\r\u000f/slush/e\u0000\u0000\u0002û ^ñú¾mmøÒ2êoš±Þ½\"6Qó\b‡ßwœa•†žp§·‡³¡_K\u0017\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000î_\f\u0000²\u0002\u0000\u0000\u0000\u0000",
"header": "0020e8216b30f547b3d47824c1cb06db9221fcd04621b0263dfe010000000000000000005f4f10270fdc9d5c868ac140c250a4aa03d64cbf52fca47570b6504e8e093b8cf853ec676c7902178d69e6ee",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 43,
"name": "Braiins Pool",
"slug": "braiinspool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 2059571,
"expectedWeight": 3991720,
"similarity": 0.9149852183486826
}
}
],
"mempool-blocks": [
{
"blockSize": 1779311,
"blockVSize": 997968.5,
"nTx": 2132,
"totalFees": 2902870,
"medianFee": 2.0479263387949875,
"feeRange": [
1.0721153846153846,
1.9980563654033041,
2.2195704057279237,
3.009493670886076,
3.4955223880597015,
6.0246913580246915,
218.1818181818182
]
},
{
"blockSize": 1959636,
"blockVSize": 997903.5,
"nTx": 497,
"totalFees": 1093076,
"medianFee": 1.102049424602265,
"feeRange": [
1.0401794819498267,
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.0761096766260911,
1.1021605957228275
]
},
{
"blockSize": 1477260,
"blockVSize": 997997.25,
"nTx": 720,
"totalFees": 1016195,
"medianFee": 1.007409072434199,
"feeRange": [
1,
1.0019120458891013,
1.0040863981319323,
1.0081019768823594,
1.018450184501845,
1.0203327171903882,
1.0485018498190837
]
},
{
"blockSize": 1021308,
"blockVSize": 431071.5,
"nTx": 823,
"totalFees": 432342,
"medianFee": 0,
"feeRange": [
1,
1,
1,
1.0028011204481793,
1.0042075736325387,
1.0053475935828877,
1.0068649885583525
]
}
]
}

View File

@ -0,0 +1,68 @@
{
"mempool-blocks": [
{
"blockSize": 1780038,
"blockVSize": 997989.75,
"nTx": 2134,
"totalFees": 2919589,
"medianFee": 2.0479263387949875,
"feeRange": [
1.0101010101010102,
2,
2.235576923076923,
3.010452961672474,
3.5240274599542336,
6.032085561497326,
218.1818181818182
]
},
{
"blockSize": 1958446,
"blockVSize": 997996,
"nTx": 503,
"totalFees": 1093277,
"medianFee": 1.102049424602265,
"feeRange": [
1.0101010101010102,
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.067677314564158,
1.0761096766260911,
1.1021605957228275
]
},
{
"blockSize": 1477611,
"blockVSize": 997927.5,
"nTx": 725,
"totalFees": 1016311,
"medianFee": 1.0075971559364956,
"feeRange": [
1,
1.0019334049409236,
1.0042075736325387,
1.0081019768823594,
1.018450184501845,
1.0203327171903882,
1.0548148148148149
]
},
{
"blockSize": 1028219,
"blockVSize": 435137,
"nTx": 833,
"totalFees": 436414,
"medianFee": 0,
"feeRange": [
1,
1,
1,
1.0028011204481793,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
{
"txReplaced": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698"
}
}

View File

@ -0,0 +1,9 @@
{
"ancestors": [],
"bestDescendant": null,
"descendants": [],
"effectiveFeePerVsize": 4.285714285714286,
"sigops": 4,
"fee": 960,
"adjustedVsize": 224
}

View File

@ -0,0 +1,36 @@
{
"replacements": {
"tx": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"fee": 960,
"vsize": 224,
"value": 49040,
"rate": 4.285714285714286,
"time": 1743541726,
"rbf": true,
"fullRbf": false
},
"time": 1743541726,
"fullRbf": false,
"replaces": [
{
"tx": {
"txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29",
"fee": 606,
"vsize": 233,
"value": 49394,
"rate": 2.6008583690987126,
"time": 1743541407,
"rbf": true
},
"time": 1743541407,
"interval": 319,
"fullRbf": false,
"replaces": []
}
]
},
"replaces": [
"242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29"
]
}

View File

@ -0,0 +1,38 @@
{
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "fb16c141d22a3a7af5e44e7478a8663866c35d554cd85107ec8a99b97e5a72e9",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj",
"value": 50000
},
"scriptsig": "483045022100ab59cf33b33eab1f76286a0fa6962c12171f2133931fec3616f96883551e6c9d02205cacbae788359f2a32ff629e732be0d95843440a3d1c222a63095f83fb69f438014104ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df",
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100ab59cf33b33eab1f76286a0fa6962c12171f2133931fec3616f96883551e6c9d02205cacbae788359f2a32ff629e732be0d95843440a3d1c222a63095f83fb69f43801 OP_PUSHBYTES_65 04ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df",
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj",
"value": 49040
}
],
"size": 224,
"weight": 896,
"sigops": 4,
"fee": 960,
"status": {
"confirmed": false
}
}

View File

@ -0,0 +1,3 @@
[
1743541726
]

View File

@ -0,0 +1,116 @@
{
"block": {
"id": "000000000000000000019bfe551da67e0d93dda4b355d16f1fdb4031ba7e5d61",
"height": 890448,
"version": 626941952,
"timestamp": 1743541850,
"bits": 386038124,
"nonce": 1177284424,
"difficulty": 113757508810854,
"merkle_root": "563d862ef4ec95ea97735ca5561e347ca6e216cb56bd451e8bedb1014078d6c3",
"tx_count": 2229,
"size": 1763153,
"weight": 3993275,
"previousblockhash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab",
"mediantime": 1743537197,
"stale": false,
"extras": {
"reward": 315498786,
"coinbaseRaw": "0350960d0f2f736c7573682fe500c702ff43d142fabe6d6def42d9effd800b9839c832dcfdc6899e137547f15c8a598dbe3a1363f999a0a310000000000000000000e9a2050064dc00000000",
"orphans": [],
"medianFee": 2.144206217858874,
"feeRange": [
1.0845921450151057,
2,
2.2448979591836733,
3,
3.5985915492957745,
6,
217.0212765957447
],
"totalFees": 2998786,
"avgFee": 1345,
"avgFeeRate": 3,
"utxoSetChange": -3073,
"avgTxSize": 790.84,
"totalInputs": 9558,
"totalOutputs": 6485,
"totalOutputAmt": 442206797883,
"segwitTotalTxs": 1986,
"segwitTotalSize": 1676431,
"segwitTotalWeight": 3646495,
"feePercentiles": null,
"virtualSize": 998318.75,
"coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11",
"coinbaseAddresses": [
"34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL",
"coinbaseSignatureAscii": "\u0003P–\r\u000f/slush/å\u0000Ç\u0002ÿCÑBú¾mmïBÙïý€\u000b˜9È2ÜýƉž\u0013uGñ\\ŠY¾:\u0013cù™ £\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000é¢\u0005\u0000dÜ\u0000\u0000\u0000\u0000",
"header": "00605e25abe2e310c10e724cfb641859658fee6570ec6062cc0400000000000000000000c3d6784001b1ed8b1e45bd56cb16e2a67c341e56a55c7397ea95ecf42e863d565a56ec676c79021748ef2b46",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 43,
"name": "Braiins Pool",
"slug": "braiinspool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 3287140,
"expectedWeight": 3991809,
"similarity": 0.9079894021278392
}
},
"mempool-blocks": [
{
"blockSize": 1979907,
"blockVSize": 997974.75,
"nTx": 461,
"totalFees": 1429178,
"medianFee": 1.1020797417745662,
"feeRange": [
1.0666666666666667,
1.0746847720659554,
1.102059530141031,
3,
3.4233409610983982,
5.017605633802817,
148.4084084084084
]
},
{
"blockSize": 1363691,
"blockVSize": 997986.5,
"nTx": 1080,
"totalFees": 1023741,
"medianFee": 1.014827018121911,
"feeRange": [
1,
1.0036011703803736,
1.0054683365672958,
1.0186757215619695,
1.0548148148148149,
1.0548148148148149,
1.068146618482189
]
},
{
"blockSize": 1337253,
"blockVSize": 563516.25,
"nTx": 901,
"totalFees": 564834,
"medianFee": 1.0028011204481793,
"feeRange": [
1,
1,
1,
1.0025062656641603,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
],
"txConfirmed": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698"
}

View File

@ -0,0 +1,116 @@
{
"block": {
"id": "000000000000000000019bfe551da67e0d93dda4b355d16f1fdb4031ba7e5d61",
"height": 890448,
"version": 626941952,
"timestamp": 1743541850,
"bits": 386038124,
"nonce": 1177284424,
"difficulty": 113757508810854,
"merkle_root": "563d862ef4ec95ea97735ca5561e347ca6e216cb56bd451e8bedb1014078d6c3",
"tx_count": 2229,
"size": 1763153,
"weight": 3993275,
"previousblockhash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab",
"mediantime": 1743537197,
"stale": false,
"extras": {
"reward": 315498786,
"coinbaseRaw": "0350960d0f2f736c7573682fe500c702ff43d142fabe6d6def42d9effd800b9839c832dcfdc6899e137547f15c8a598dbe3a1363f999a0a310000000000000000000e9a2050064dc00000000",
"orphans": [],
"medianFee": 2.144206217858874,
"feeRange": [
1.0845921450151057,
2,
2.2448979591836733,
3,
3.5985915492957745,
6,
217.0212765957447
],
"totalFees": 2998786,
"avgFee": 1345,
"avgFeeRate": 3,
"utxoSetChange": -3073,
"avgTxSize": 790.84,
"totalInputs": 9558,
"totalOutputs": 6485,
"totalOutputAmt": 442206797883,
"segwitTotalTxs": 1986,
"segwitTotalSize": 1676431,
"segwitTotalWeight": 3646495,
"feePercentiles": null,
"virtualSize": 998318.75,
"coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11",
"coinbaseAddresses": [
"34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL",
"coinbaseSignatureAscii": "\u0003P–\r\u000f/slush/å\u0000Ç\u0002ÿCÑBú¾mmïBÙïý€\u000b˜9È2ÜýƉž\u0013uGñ\\ŠY¾:\u0013cù™ £\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000é¢\u0005\u0000dÜ\u0000\u0000\u0000\u0000",
"header": "00605e25abe2e310c10e724cfb641859658fee6570ec6062cc0400000000000000000000c3d6784001b1ed8b1e45bd56cb16e2a67c341e56a55c7397ea95ecf42e863d565a56ec676c79021748ef2b46",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 43,
"name": "Braiins Pool",
"slug": "braiinspool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 3287140,
"expectedWeight": 3991809,
"similarity": 0.9079894021278392
}
},
"mempool-blocks": [
{
"blockSize": 1979907,
"blockVSize": 997974.75,
"nTx": 461,
"totalFees": 1429178,
"medianFee": 1.1020797417745662,
"feeRange": [
1.0666666666666667,
1.0746847720659554,
1.102059530141031,
3,
3.4233409610983982,
5.017605633802817,
148.4084084084084
]
},
{
"blockSize": 1363691,
"blockVSize": 997986.5,
"nTx": 1080,
"totalFees": 1023741,
"medianFee": 1.014827018121911,
"feeRange": [
1,
1.0036011703803736,
1.0054683365672958,
1.0186757215619695,
1.0548148148148149,
1.0548148148148149,
1.068146618482189
]
},
{
"blockSize": 1337253,
"blockVSize": 563516.25,
"nTx": 901,
"totalFees": 564834,
"medianFee": 1.0028011204481793,
"feeRange": [
1,
1,
1,
1.0025062656641603,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
],
"txConfirmed": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698"
}

View File

@ -0,0 +1,75 @@
{
"mempool-blocks": [
{
"blockSize": 1779823,
"blockVSize": 997995.25,
"nTx": 2133,
"totalFees": 2922926,
"medianFee": 2.0479263387949875,
"feeRange": [
1.0825892857142858,
2,
2.2439024390243905,
3.010452961672474,
3.554973821989529,
6.032085561497326,
218.1818181818182
]
},
{
"blockSize": 1957833,
"blockVSize": 997953,
"nTx": 500,
"totalFees": 1093270,
"medianFee": 1.102049424602265,
"feeRange": [
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.067677314564158,
1.0766488413547237,
1.1021605957228275
]
},
{
"blockSize": 1477864,
"blockVSize": 997999,
"nTx": 730,
"totalFees": 1016458,
"medianFee": 1.0075971559364956,
"feeRange": [
1,
1.0019552465783186,
1.004255319148936,
1.0081019768823594,
1.018450184501845,
1.0203327171903882,
1.0548148148148149
]
},
{
"blockSize": 1030954,
"blockVSize": 436613.5,
"nTx": 838,
"totalFees": 437891,
"medianFee": 0,
"feeRange": [
1,
1,
1,
1.0026525198938991,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
],
"txPosition": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"position": {
"block": 0,
"vsize": 111102
}
}
}

View File

@ -0,0 +1,75 @@
{
"mempool-blocks": [
{
"blockSize": 1719945,
"blockVSize": 997952.25,
"nTx": 2558,
"totalFees": 3287140,
"medianFee": 2.4046448299072485,
"feeRange": [
1.073446327683616,
2,
2.2567567567567566,
3.0106761565836297,
3.6169014084507043,
6.015037593984962,
218.1818181818182
]
},
{
"blockSize": 2022898,
"blockVSize": 997983.25,
"nTx": 131,
"totalFees": 1098129,
"medianFee": 1.1020797417745662,
"feeRange": [
1.0625,
1.0691217722793642,
1.073436083408885,
1.0761096766260911,
1.080091533180778,
1.102110739151618,
1.1021909190121146
]
},
{
"blockSize": 1363844,
"blockVSize": 997998.5,
"nTx": 1073,
"totalFees": 1023651,
"medianFee": 1.014827018121911,
"feeRange": [
1,
1.003584229390681,
1.0054683365672958,
1.0186757215619695,
1.0548148148148149,
1.0548148148148149,
1.068146618482189
]
},
{
"blockSize": 1335390,
"blockVSize": 562453.5,
"nTx": 902,
"totalFees": 563772,
"medianFee": 1.0028011204481793,
"feeRange": [
1,
1,
1,
1.0025402201524132,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
],
"txPosition": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"position": {
"block": 0,
"vsize": 128920
}
}
}

View File

@ -0,0 +1,9 @@
{
"txPosition": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"position": {
"block": 0,
"vsize": 110880
}
}
}

View File

@ -0,0 +1,37 @@
{
"rbfLatest": [
{
"tx": {
"txid": "f4bae4f626036250fd00d68490e572f65f66417452003a0f4c4d76f17a9fde68",
"fee": 1185,
"vsize": 223,
"value": 41729,
"rate": 5.313901345291479,
"time": 1743587177,
"rbf": true,
"fullRbf": false,
"mined": true
},
"time": 1743587177,
"fullRbf": true,
"replaces": [
{
"tx": {
"txid": "12945412dfc455e0ed6049dc2ee8737756c8d9e2d9a2eb26f366cd5019a0369f",
"fee": 504,
"vsize": 222,
"value": 42410,
"rate": 2.27027027027027,
"time": 1743586081,
"rbf": true
},
"time": 1743586081,
"interval": 1096,
"fullRbf": false,
"replaces": []
}
],
"mined": true
}
]
}

View File

@ -0,0 +1,68 @@
{
"rbfLatest": [
{
"tx": {
"txid": "d313b479acfbae719afb488a078e0fe0e052a67b9f65f73f7c75d3d95fd36acc",
"fee": 672,
"vsize": 167.25,
"value": 29996328,
"rate": 4.017937219730942,
"time": 1743587365,
"rbf": true,
"fullRbf": false
},
"time": 1743587365,
"fullRbf": false,
"replaces": [
{
"tx": {
"txid": "eb5aa786cabda307cc9642cfb9c41a3b405ac20a391eefbe54be7930bea61865",
"fee": 336,
"vsize": 167.5,
"value": 29996664,
"rate": 2.005970149253731,
"time": 1743586424,
"rbf": true
},
"time": 1743586424,
"interval": 941,
"fullRbf": false,
"replaces": []
}
]
},
{
"tx": {
"txid": "f4bae4f626036250fd00d68490e572f65f66417452003a0f4c4d76f17a9fde68",
"fee": 1185,
"vsize": 223,
"value": 41729,
"rate": 5.313901345291479,
"time": 1743587177,
"rbf": true,
"fullRbf": false,
"mined": true
},
"time": 1743587177,
"fullRbf": true,
"replaces": [
{
"tx": {
"txid": "12945412dfc455e0ed6049dc2ee8737756c8d9e2d9a2eb26f366cd5019a0369f",
"fee": 504,
"vsize": 222,
"value": 42410,
"rate": 2.27027027027027,
"time": 1743586081,
"rbf": true
},
"time": 1743586081,
"interval": 1096,
"fullRbf": false,
"replaces": []
}
],
"mined": true
}
]
}

View File

@ -44,6 +44,7 @@
import { PageIdleDetector } from './PageIdleDetector';
import { mockWebSocket } from './websocket';
import { mockWebSocketV2 } from './websocket';
/* global Cypress */
const codes = {
@ -72,6 +73,10 @@ Cypress.Commands.add('mockMempoolSocket', () => {
mockWebSocket();
});
Cypress.Commands.add('mockMempoolSocketV2', () => {
mockWebSocketV2();
});
Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => {
cy.get('.dropdown-toggle').click().then(() => {
cy.get(`a.${network}`).click().then(() => {

View File

@ -5,6 +5,7 @@ declare namespace Cypress {
waitForSkeletonGone(): Chainable<any>
waitForPageIdle(): Chainable<any>
mockMempoolSocket(): Chainable<any>
mockMempoolSocketV2(): Chainable<any>
changeNetwork(network: "testnet"|"testnet4"|"signet"|"liquid"|"mainnet"): Chainable<any>
}
}

View File

@ -27,6 +27,37 @@ const createMock = (url: string) => {
return mocks[url];
};
export const mockWebSocketV2 = () => {
cy.on('window:before:load', (win) => {
const winWebSocket = win.WebSocket;
cy.stub(win, 'WebSocket').callsFake((url) => {
console.log(url);
if ((new URL(url).pathname.indexOf('/sockjs-node/') !== 0)) {
const { server, websocket } = createMock(url);
win.mockServer = server;
win.mockServer.on('connection', (socket) => {
win.mockSocket = socket;
});
win.mockServer.on('message', (message) => {
console.log(message);
});
return websocket;
} else {
return new winWebSocket(url);
}
});
});
cy.on('window:before:unload', () => {
for (const url in mocks) {
cleanupMock(url);
}
});
};
export const mockWebSocket = () => {
cy.on('window:before:load', (win) => {
const winWebSocket = win.WebSocket;
@ -65,6 +96,27 @@ export const mockWebSocket = () => {
});
};
export const receiveWebSocketMessageFromServer = ({
params
}: { params?: any } = {}) => {
cy.window().then((win) => {
if (params.message) {
console.log('sending message');
win.mockSocket.send(params.message.contents);
}
if (params.file) {
cy.readFile(`cypress/fixtures/${params.file.path}`, 'utf-8').then((fixture) => {
console.log('sending payload');
win.mockSocket.send(JSON.stringify(fixture));
});
}
});
return;
};
export const emitMempoolInfo = ({
params
}: { params?: any } = {}) => {
@ -82,16 +134,22 @@ export const emitMempoolInfo = ({
switch (params.command) {
case "init": {
win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => {
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'utf-8').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'ascii').then((fixture) => {
cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'utf-8').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
break;
}
case "rbfTransaction": {
cy.readFile('cypress/fixtures/mainnet_rbf.json', 'ascii').then((fixture) => {
cy.readFile('cypress/fixtures/mainnet_rbf.json', 'utf-8').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
break;
}
case 'trackTx': {
cy.readFile('cypress/fixtures/track_tx.json', 'utf-8').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
break;

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "3.1.0-dev",
"version": "3.2.0",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@ -25,14 +25,12 @@
"i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf",
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
"serve": "npm run generate-config && npm run ng -- serve -c local",
"serve:stg": "npm run generate-config && npm run ng -- serve -c staging",
"serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod",
"serve:local-staging": "npm run generate-config && npm run ng -- serve -c local-staging",
"serve:parameterized": "npm run generate-config && npm run ng -- serve -c parameterized",
"start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local",
"start:parameterized": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c parameterized",
"start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora",
"start:stg": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c staging",
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets && npm run build-mempool.js",
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
@ -58,8 +56,8 @@
"cypress:run:record": "cypress run --record",
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
"cypress:open:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:open",
"cypress:run:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^17.3.1",

View File

@ -0,0 +1,36 @@
const fs = require('fs');
const PROXY_CONFIG = require('./proxy.conf');
const addApiKeyHeader = (proxyReq, req, res) => {
if (process.env.MEMPOOL_CI_API_KEY) {
proxyReq.setHeader('X-Mempool-Auth', process.env.MEMPOOL_CI_API_KEY);
}
};
PROXY_CONFIG.forEach((entry) => {
const mempoolHostname = process.env.MEMPOOL_HOSTNAME
? process.env.MEMPOOL_HOSTNAME
: 'mempool.space';
const liquidHostname = process.env.LIQUID_HOSTNAME
? process.env.LIQUID_HOSTNAME
: 'liquid.network';
entry.target = entry.target.replace('mempool.space', mempoolHostname);
entry.target = entry.target.replace('liquid.network', liquidHostname);
if (entry.onProxyReq) {
const originalProxyReq = entry.onProxyReq;
entry.onProxyReq = (proxyReq, req, res) => {
originalProxyReq(proxyReq, req, res);
if (process.env.MEMPOOL_CI_API_KEY) {
proxyReq.setHeader('X-Mempool-Auth', process.env.MEMPOOL_CI_API_KEY);
}
};
} else {
entry.onProxyReq = addApiKeyHeader;
}
});
module.exports = PROXY_CONFIG;

View File

@ -1,12 +0,0 @@
const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => {
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space';
console.log(`e2e tests running against ${hostname}`);
entry.target = entry.target.replace("mempool.space", hostname);
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
});
module.exports = PROXY_CONFIG;

View File

@ -11,6 +11,7 @@
<div class="about-text">
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &reg;</ng-template></h5>
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
<h5>Be your own explorer&trade;</h5>
</div>
<video #promoVideo (click)="unmutePromoVideo()" (touchstart)="unmutePromoVideo()" src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
@ -145,6 +146,15 @@
</svg>
<span>Bull Bitcoin</span>
</a>
<a href="https://fortris.com/" target="_blank" title="Fortris">
<svg id="fortris-logo" viewBox="0 0 140.08 129.13" version="1.1" width="74px" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs><style>.fortris-cls-1{fill:#29384a;}.fortris-cls-1,.fortris-cls-2{stroke-width:0px;}.fortris-cls-2{fill:#ff6e72;}</style></defs>
<rect class="fortris-cls-2" x="0" y="0" width="140.08" height="30.59" rx="15.29" ry="15.29" />
<rect class="fortris-cls-2" x="0" y="98.540001" width="69.779999" height="30.59" rx="15.29" ry="15.29" />
<rect class="fortris-cls-2" x="0" y="49.27" width="109.5" height="30.59" rx="15.29" ry="15.29" />
</svg>
<span>Fortris</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
@ -449,7 +459,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@ -264,6 +264,6 @@
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
max-width: 850px;
}
}
}

View File

@ -158,7 +158,7 @@
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator fees</td>
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator® fees</td>
</tr>
<tr class="info" [class.group-last]="!estimate.vsizeFee" [class.dashed-bottom]="!estimate.vsizeFee">
<td class="info">
@ -567,14 +567,29 @@
} @else if (step === 'success') {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;"><ng-content select="[slot='accelerated-title']"></ng-content><span class="default-slot" i18n="accelerator.success-message">Your transaction is being accelerated!</span></h1>
<h1 style="font-size: larger;"><ng-content select="[slot='accelerated-title']"></ng-content>
@if (accelerationResponse) {
<span class="default-slot" i18n="accelerator.success-message">Your transaction is being accelerated!</span>
} @else {
<span class="default-slot" i18n="accelerator.success-message-third-party">Transaction is already being accelerated!</span>
}
</h1>
</div>
</div>
<div class="row text-center mt-1">
<div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center">
<span i18n="accelerator.confirmed-acceleration-with-miners">Your transaction has been accepted for acceleration by our mining pool partners.</span>
@if (accelerationResponse) {
<span i18n="accelerator.confirmed-acceleration-with-miners">Your transaction has been accepted for acceleration by our mining pool partners.</span>
} @else {
<span i18n="accelerator.confirmed-acceleration-with-miners-third-party">Transaction has already been accepted for acceleration by our mining pool partners.</span>
}
</div>
@if (accelerationResponse?.receiptUrl) {
<div class="d-flex flex-row justify-content-center align-items-center">
<span i18n="accelerator.receipt-label"><a [href]="accelerationResponse.receiptUrl" target="_blank">Click here to get a receipt.</a></span>
</div>
}
</div>
</div>
<hr>

View File

@ -87,6 +87,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
math = Math;
isMobile: boolean = window.innerWidth <= 767.98;
isProdDomain = false;
accelerationResponse: { receiptUrl: string | null } | undefined;
private _step: CheckoutStep = 'summary';
simpleMode: boolean = true;
@ -194,16 +195,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.scrollToElement('acceleratePreviewAnchor', 'start');
}
if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success', true);
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
}
this.moveToStep('success', true);
}
}
moveToStep(step: CheckoutStep, force: boolean = false): void {
if (this.isCheckoutLocked > 0 && !force) {
if (this.isCheckoutLocked > 0 && !force || this.step === 'success') {
return;
}
this.processing = false;
@ -525,7 +522,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
console.error(`Cannot retrieve payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
@ -541,7 +538,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD
).subscribe({
next: () => {
next: (response) => {
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
@ -643,7 +641,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
console.error(`Cannot retrieve payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
@ -668,7 +666,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
next: (response) => {
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
@ -777,7 +776,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
next: (response) => {
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
@ -870,7 +870,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.details.cashAppPay.referenceId,
costUSD
).subscribe({
next: () => {
next: (response) => {
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
@ -936,7 +937,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingBtcpayInvoice = true;
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
return this.servicesApiService.retrieveInvoice$(response.btcpayInvoiceId);
}),
catchError(error => {
console.log(error);

View File

@ -44,6 +44,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() right: number | string = 10;
@Input() left: number | string = 70;
@Input() widget: boolean = false;
@Input() label: string = '';
@Input() defaultFiat: boolean = false;
@Input() showLegend: boolean = true;
@Input() showYAxis: boolean = true;
@ -55,6 +56,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
hoverData: any[] = [];
conversions: any;
allowZoom: boolean = false;
labelGraphic: any;
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
@ -85,6 +87,18 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
this.labelGraphic = this.label ? {
type: 'text',
right: '36px',
bottom: '36px',
z: 100,
silent: true,
style: {
fill: '#fff',
text: this.label,
font: '24px sans-serif'
}
} : undefined;
if (!this.addressSummary$ && (!this.address || !this.stats)) {
return;
}
@ -205,6 +219,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
right: this.adjustedRight,
left: this.adjustedLeft,
},
graphic: this.labelGraphic ? [{
...this.labelGraphic,
right: this.adjustedRight + 22 + 'px',
}] : undefined,
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
data: [
{
@ -443,6 +461,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
right: this.adjustedRight,
left: this.adjustedLeft,
},
graphic: this.labelGraphic ? [{
...this.labelGraphic,
right: this.adjustedRight + 22 + 'px',
}] : undefined,
legend: {
selected: this.selected,
},

View File

@ -36,6 +36,8 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
sent = 0;
totalUnspent = 0;
ogSession: number;
constructor(
private route: ActivatedRoute,
private electrsApiService: ElectrsApiService,
@ -58,7 +60,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
.pipe(
switchMap((params: ParamMap) => {
this.rawAddress = params.get('id') || '';
this.openGraphService.waitFor('address-data-' + this.rawAddress);
this.ogSession = this.openGraphService.waitFor('address-data-' + this.rawAddress);
this.error = undefined;
this.isLoadingAddress = true;
this.loadedConfirmedTxCount = 0;
@ -79,7 +81,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.isLoadingAddress = false;
this.error = err;
console.log(err);
this.openGraphService.fail('address-data-' + this.rawAddress);
this.openGraphService.fail({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession });
return of(null);
})
);
@ -97,7 +99,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.address = address;
this.updateChainStats();
this.isLoadingAddress = false;
this.openGraphService.waitOver('address-data-' + this.rawAddress);
this.openGraphService.waitOver({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession });
})
)
.subscribe(() => {},
@ -105,7 +107,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
console.log(error);
this.error = error;
this.isLoadingAddress = false;
this.openGraphService.fail('address-data-' + this.rawAddress);
this.openGraphService.fail({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession });
}
);
}

View File

@ -49,7 +49,7 @@
</ng-template>
</tr>
</ng-template>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet' && block?.extras?.pool">
<td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">

View File

@ -35,6 +35,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
overviewSubscription: Subscription;
networkChangedSubscription: Subscription;
ogSession: number;
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
constructor(
@ -53,8 +55,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
const block$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.rawId = params.get('id') || '';
this.openGraphService.waitFor('block-viz-' + this.rawId);
this.openGraphService.waitFor('block-data-' + this.rawId);
this.ogSession = this.openGraphService.waitFor('block-viz-' + this.rawId);
this.ogSession = this.openGraphService.waitFor('block-data-' + this.rawId);
const blockHash: string = params.get('id') || '';
this.block = undefined;
@ -86,8 +88,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
catchError((err) => {
this.error = err;
this.seoService.logSoft404();
this.openGraphService.fail('block-data-' + this.rawId);
this.openGraphService.fail('block-viz-' + this.rawId);
this.openGraphService.fail({ event: 'block-data-' + this.rawId, sessionId: this.ogSession });
this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
return of(null);
}),
);
@ -114,7 +116,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
this.isLoadingOverview = true;
this.overviewError = null;
this.openGraphService.waitOver('block-data-' + this.rawId);
this.openGraphService.waitOver({ event: 'block-data-' + this.rawId, sessionId: this.ogSession });
}),
throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
shareReplay({ bufferSize: 1, refCount: true })
@ -129,7 +131,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
.pipe(
catchError((err) => {
this.overviewError = err;
this.openGraphService.fail('block-viz-' + this.rawId);
this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
return of([]);
}),
switchMap((transactions) => {
@ -138,7 +140,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
),
this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
.pipe(
catchError(() => {
return of([]);
}))
: of([])
@ -169,8 +172,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
this.error = error;
this.isLoadingOverview = false;
this.seoService.logSoft404();
this.openGraphService.fail('block-viz-' + this.rawId);
this.openGraphService.fail('block-data-' + this.rawId);
this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
this.openGraphService.fail({ event: 'block-data-' + this.rawId, sessionId: this.ogSession });
if (this.blockGraph) {
this.blockGraph.destroy();
}
@ -196,6 +199,6 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
}
onGraphReady(): void {
this.openGraphService.waitOver('block-viz-' + this.rawId);
this.openGraphService.waitOver({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
}
}

View File

@ -238,7 +238,7 @@
<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" [defaultFiat]="true" [height]="graphHeight"></app-address-graph>
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [defaultFiat]="true" [height]="graphHeight" [label]="widget.props.label"></app-address-graph>
</div>
</div>
</div>
@ -272,7 +272,7 @@
<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 [addressSummary$]="walletSummary$" [period]="widget.props.period || 'all'" [widget]="true" [height]="graphHeight"></app-address-graph>
<app-address-graph [addressSummary$]="walletSummary$" [period]="widget.props.period || 'all'" [widget]="true" [height]="graphHeight" [label]="widget.props.label"></app-address-graph>
</div>
</div>
</div>

View File

@ -70,11 +70,6 @@
<li class="nav-item" routerLinkActive="active" id="btn-graphs">
<a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
</li>
<!--
<li class="nav-item d-none d-lg-block" routerLinkActive="active">
<a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon></a>
</li>
-->
<li class="nav-item" routerLinkActive="active" id="btn-assets">
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
</li>

View File

@ -30,6 +30,8 @@ export class PoolPreviewComponent implements OnInit {
slug: string = undefined;
ogSession: number;
constructor(
@Inject(LOCALE_ID) public locale: string,
private apiService: ApiService,
@ -47,22 +49,22 @@ export class PoolPreviewComponent implements OnInit {
this.isLoading = true;
this.imageLoaded = false;
this.slug = slug;
this.openGraphService.waitFor('pool-hash-' + this.slug);
this.openGraphService.waitFor('pool-stats-' + this.slug);
this.openGraphService.waitFor('pool-chart-' + this.slug);
this.openGraphService.waitFor('pool-img-' + this.slug);
this.ogSession = this.openGraphService.waitFor('pool-hash-' + this.slug);
this.ogSession = this.openGraphService.waitFor('pool-stats-' + this.slug);
this.ogSession = this.openGraphService.waitFor('pool-chart-' + this.slug);
this.ogSession = this.openGraphService.waitFor('pool-img-' + this.slug);
return this.apiService.getPoolHashrate$(this.slug)
.pipe(
switchMap((data) => {
this.isLoading = false;
this.prepareChartOptions(data.map(val => [val.timestamp * 1000, val.avgHashrate]));
this.openGraphService.waitOver('pool-hash-' + this.slug);
this.openGraphService.waitOver({ event: 'pool-hash-' + this.slug, sessionId: this.ogSession });
return [slug];
}),
catchError(() => {
this.isLoading = false;
this.seoService.logSoft404();
this.openGraphService.fail('pool-hash-' + this.slug);
this.openGraphService.fail({ event: 'pool-hash-' + this.slug, sessionId: this.ogSession });
return of([slug]);
})
);
@ -72,7 +74,7 @@ export class PoolPreviewComponent implements OnInit {
catchError(() => {
this.isLoading = false;
this.seoService.logSoft404();
this.openGraphService.fail('pool-stats-' + this.slug);
this.openGraphService.fail({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession });
return of(null);
})
);
@ -90,11 +92,11 @@ export class PoolPreviewComponent implements OnInit {
}
poolStats.pool.regexes = regexes.slice(0, -3);
this.openGraphService.waitOver('pool-stats-' + this.slug);
this.openGraphService.waitOver({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession });
const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg';
if (logoSrc === this.lastImgSrc) {
this.openGraphService.waitOver('pool-img-' + this.slug);
this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession });
}
this.lastImgSrc = logoSrc;
return Object.assign({
@ -103,7 +105,7 @@ export class PoolPreviewComponent implements OnInit {
}),
catchError(() => {
this.isLoading = false;
this.openGraphService.fail('pool-stats-' + this.slug);
this.openGraphService.fail({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession });
return of(null);
})
);
@ -170,16 +172,16 @@ export class PoolPreviewComponent implements OnInit {
}
onChartReady(): void {
this.openGraphService.waitOver('pool-chart-' + this.slug);
this.openGraphService.waitOver({ event: 'pool-chart-' + this.slug, sessionId: this.ogSession });
}
onImageLoad(): void {
this.imageLoaded = true;
this.openGraphService.waitOver('pool-img-' + this.slug);
this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession });
}
onImageFail(): void {
this.imageLoaded = false;
this.openGraphService.waitOver('pool-img-' + this.slug);
this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession });
}
}

View File

@ -84,74 +84,79 @@ div.scrollable {
text-align: right;
}
}
}
.progress {
background-color: var(--secondary);
.progress {
background-color: var(--secondary);
}
.coinbase {
width: 20%;
@media (max-width: 875px) {
display: none;
}
}
.coinbase {
width: 20%;
@media (max-width: 875px) {
display: none;
}
.health {
@media (max-width: 1150px) {
display: none;
}
}
.height {
width: 10%;
.height {
width: 10%;
}
.timestamp {
@media (max-width: 875px) {
padding-left: 50px;
}
.timestamp {
@media (max-width: 875px) {
padding-left: 50px;
}
@media (max-width: 685px) {
display: none;
}
@media (max-width: 625px) {
display: none;
}
}
.mined {
width: 13%;
@media (max-width: 1100px) {
display: none;
}
.mined {
width: 13%;
}
.txs {
@media (max-width: 938px) {
display: none;
}
.txs {
padding-right: 40px;
@media (max-width: 1100px) {
padding-right: 10px;
}
@media (max-width: 875px) {
padding-right: 20px;
}
@media (max-width: 567px) {
padding-right: 10px;
}
padding-right: 40px;
@media (max-width: 1100px) {
padding-right: 10px;
}
.size {
width: 12%;
@media (max-width: 1000px) {
width: 15%;
}
@media (max-width: 875px) {
width: 20%;
}
@media (max-width: 650px) {
width: 20%;
}
@media (max-width: 450px) {
display: none;
}
@media (max-width: 875px) {
padding-right: 20px;
}
@media (max-width: 567px) {
padding-right: 10px;
}
}
.scriptmessage {
overflow: hidden;
display: inline-block;
text-overflow: ellipsis;
vertical-align: middle;
width: auto;
text-align: left;
.size {
min-width: 80px;
width: 12%;
@media (max-width: 1000px) {
width: 15%;
}
}
.scriptmessage {
max-width: 340px;
overflow: hidden;
display: inline-block;
text-overflow: ellipsis;
vertical-align: middle;
width: auto;
text-align: left;
}
.reward {
@media (max-width: 1035px) {
display: none;
}
}

View File

@ -45,14 +45,14 @@
<br>
<h4>USING MEMPOOL ACCELERATOR&trade;</h4>
<h4>USING MEMPOOL ACCELERATOR&reg;</h4>
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator&trade; your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator&reg; your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
<p *ngIf="!officialMempoolSpace">When using Mempool Accelerator&trade; the mempool.space privacy policy will apply: <a href="https://mempool.space/privacy-policy">https://mempool.space/privacy-policy</a>.</p>
<p *ngIf="!officialMempoolSpace">When using Mempool Accelerator&reg; the mempool.space privacy policy will apply: <a href="https://mempool.space/privacy-policy">https://mempool.space/privacy-policy</a>.</p>
<br>
<ng-container *ngIf="officialMempoolSpace">
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
@ -67,7 +67,7 @@
<li>If you sign up for a subscription to Mempool Enterprise&trade; we also collect your company name which is not shared with any third-party.</li>
<li>If you sign up for an account on mempool.space and use Mempool Accelerator&trade; Pro your accelerated transactions will be associated with your account for the purposes of accounting.</li>
<li>If you sign up for an account on mempool.space and use Mempool Accelerator&reg; Pro your accelerated transactions will be associated with your account for the purposes of accounting.</li>
</ul>
@ -101,7 +101,7 @@
<p>We aim to retain your data only as long as necessary:</p>
<ul>
<li>An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator Pro account credit</li>
<li>An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator® Pro account credit</li>
<li>If an account meets the criteria for inactivity as defined above, we will automatically delete the associated account data after a period of 6 months of continuous inactivity, except in the case of payment disputes or account irregularities.</li>
</ul>

View File

@ -194,14 +194,16 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
applyScrollLeft(): void {
if (this.blockchainContainer?.nativeElement?.scrollWidth) {
let lastScrollLeft = null;
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft += this.pageWidth;
}
lastScrollLeft = null;
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft -= this.pageWidth;
if (!this.timeLtr) {
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft += this.pageWidth;
}
lastScrollLeft = null;
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft -= this.pageWidth;
}
}
this.blockchainContainer.nativeElement.scrollLeft = this.scrollLeft;
}

View File

@ -16,9 +16,6 @@
<a class="btn btn-primary btn-sm mb-0" [routerLink]="['/clock/mempool/0' | relativeUrl]" style="color: white" id="btn-clock">
<fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" i18n-title="footer.clock-mempool" title="Clock (Mempool)"></fa-icon>
</a>
<a *ngIf="!isMobile()" class="btn btn-primary btn-sm mb-0" [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
</a>
</div>
<div class="btn-toggle-rows" name="radioBasic">
<div class="btn-group btn-group-toggle">

File diff suppressed because one or more lines are too long

View File

@ -1,25 +0,0 @@
<div id="tv-wrapper">
<div class="tv-container">
<div class="chart-holder">
<app-mempool-graph
[template]="'advanced'"
[height]="600"
[left]="60"
[right]="10"
[data]="statsSubscription$ | async"
[showZoom]="false"
></app-mempool-graph>
</div>
<div class="blockchain-wrapper" [dir]="timeLtr ? 'rtl' : 'ltr'" [class.time-ltr]="timeLtr">
<div class="position-container">
<span>
<div class="blocks-wrapper">
<app-mempool-blocks></app-mempool-blocks>
<app-blockchain-blocks></app-blockchain-blocks>
</div>
<div id="divider"></div>
</span>
</div>
</div>
</div>
</div>

View File

@ -1,80 +0,0 @@
.loading {
margin: auto;
width: 100%;
display: flex;
text-align: center;
justify-content: center;
height: 100vh;
align-items: center;
}
#tv-wrapper {
height: 100vh;
overflow: hidden;
position: relative;
}
.chart-holder {
position: relative;
height: 655px;
width: 100%;
margin: 30px auto 0;
}
.blockchain-wrapper {
display: block;
height: 100%;
min-height: 240px;
position: relative;
top: 30px;
.position-container {
position: absolute;
left: 0;
bottom: 170px;
transform: translateX(50vw);
}
#divider {
width: 2px;
height: 175px;
left: 0;
top: -40px;
position: absolute;
img {
position: absolute;
left: -100px;
top: -28px;
}
}
&.time-ltr {
.blocks-wrapper {
transform: scaleX(-1);
}
}
}
:host-context(.ltr-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: ltr;
}
}
:host-context(.rtl-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: rtl;
}
}
.tv-container {
display: flex;
margin-top: 0px;
flex-direction: column;
}

View File

@ -1,86 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { WebsocketService } from '@app/services/websocket.service';
import { OptimizedMempoolStats } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { ApiService } from '@app/services/api.service';
import { SeoService } from '@app/services/seo.service';
import { ActivatedRoute } from '@angular/router';
import { map, scan, startWith, switchMap, tap } from 'rxjs/operators';
import { interval, merge, Observable, Subscription } from 'rxjs';
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-television',
templateUrl: './television.component.html',
styleUrls: ['./television.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TelevisionComponent implements OnInit, OnDestroy {
mempoolStats: OptimizedMempoolStats[] = [];
statsSubscription$: Observable<OptimizedMempoolStats[]>;
fragment: string;
timeLtrSubscription: Subscription;
timeLtr: boolean = this.stateService.timeLtr.value;
constructor(
private websocketService: WebsocketService,
private apiService: ApiService,
private stateService: StateService,
private seoService: SeoService,
private route: ActivatedRoute
) { }
refreshStats(time: number, fn: Observable<OptimizedMempoolStats[]>) {
return interval(time).pipe(startWith(0), switchMap(() => fn));
}
ngOnInit() {
this.seoService.setTitle($localize`:@@46ce8155c9ab953edeec97e8950b5a21e67d7c4e:TV view`);
this.seoService.setDescription($localize`:@@meta.description.tv:See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV.`);
this.websocketService.want(['blocks', 'live-2h-chart', 'mempool-blocks']);
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
this.timeLtr = !!ltr;
});
this.statsSubscription$ = merge(
this.stateService.live2Chart$.pipe(map(stats => [stats])),
this.route.fragment
.pipe(
tap(fragment => { this.fragment = fragment ?? '2h'; }),
switchMap((fragment) => {
const minute = 60000; const hour = 3600000;
switch (fragment) {
case '24h': return this.apiService.list24HStatistics$();
case '1w': return this.refreshStats(5 * minute, this.apiService.list1WStatistics$());
case '1m': return this.refreshStats(30 * minute, this.apiService.list1MStatistics$());
case '3m': return this.refreshStats(2 * hour, this.apiService.list3MStatistics$());
case '6m': return this.refreshStats(3 * hour, this.apiService.list6MStatistics$());
case '1y': return this.refreshStats(8 * hour, this.apiService.list1YStatistics$());
case '2y': return this.refreshStats(8 * hour, this.apiService.list2YStatistics$());
case '3y': return this.refreshStats(12 * hour, this.apiService.list3YStatistics$());
default /* 2h */: return this.apiService.list2HStatistics$();
}
})
)
)
.pipe(
scan((mempoolStats, newStats) => {
if (newStats.length > 1) {
mempoolStats = newStats;
} else if (['2h', '24h'].includes(this.fragment)) {
mempoolStats.unshift(newStats[0]);
const now = Date.now() / 1000;
const start = now - (this.fragment === '2h' ? (2 * 60 * 60) : (24 * 60 * 60) );
mempoolStats = mempoolStats.filter(p => p.added >= start);
}
return mempoolStats;
})
);
}
ngOnDestroy() {
this.timeLtrSubscription.unsubscribe();
}
}

View File

@ -67,9 +67,9 @@
</ng-container>
<h4>MEMPOOL ACCELERATOR&trade;</h4>
<h4>MEMPOOL ACCELERATOR&reg;</h4>
<p><a href="https://mempool.space/accelerator">Mempool Accelerator&trade;</a> enables members of the Bitcoin community to submit requests for transaction prioritization. </p>
<p><a href="https://mempool.space/accelerator">Mempool Accelerator&reg;</a> enables members of the Bitcoin community to submit requests for transaction prioritization. </p>
<ul>
<li>Mempool will use reasonable commercial efforts to relay user acceleration requests to Mempool's mining pool partners, but it is at the discretion of Mempool and Mempool's mining pool partners to accept acceleration requests. </li>
@ -84,11 +84,11 @@
<br>
<li>All acceleration payments and Mempool Accelerator&trade; account credit top-ups are non-refundable. </li>
<li>All acceleration payments and Mempool Accelerator&reg; account credit top-ups are non-refundable. </li>
<br>
<li>Mempool Accelerator&trade; account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.</li>
<li>Mempool Accelerator&reg; account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.</li>
<br>

View File

@ -8,7 +8,7 @@
<div *ngIf="officialMempoolSpace">
<h2>Trademark Policy and Guidelines</h2>
<h5>The Mempool Open Source Project &reg;</h5>
<h6>Updated: August 19, 2024</h6>
<h6>Updated: February 11, 2025</h6>
<br>
<div class="text-left">
@ -59,6 +59,7 @@
<tr><td>Mempool Accelerator</td></tr>
<tr><td>Mempool Enterprise</td></tr>
<tr><td>Mempool Liquidity</td></tr>
<tr><td>Mempool</td></tr>
<tr><td>mempool.space</td></tr>
<tr><td>Be your own explorer</td></tr>
<tr><td>Explore the full Bitcoin ecosystem</td></tr>
@ -340,7 +341,8 @@
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, Mempool&reg;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<li>What to Do When You See Abuse</li>
<br>

View File

@ -0,0 +1,56 @@
<br>
<div class="title">
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
</div>
<div class="box cpfp-details">
<table class="table table-fixed table-borderless table-striped">
<thead>
<tr>
<th i18n="transactions-list.vout.scriptpubkey-type">Type</th>
<th class="txids" i18n="dashboard.latest-transactions.txid">TXID</th>
<th *only-vsize class="d-none d-lg-table-cell" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</th>
<th *only-weight class="d-none d-lg-table-cell" i18n="transaction.weight|Transaction Weight">Weight</th>
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
<th class="d-none d-lg-table-cell"></th>
</tr>
</thead>
<tbody>
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td>
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) > roundToOneDecimal(tx)" class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
<tr>
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td class="txids">
<app-truncate [text]="cpfpInfo.bestDescendant.txid" [link]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpInfo.bestDescendant.fee" [weight]="cpfpInfo.bestDescendant.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
<td class="txids">
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) < roundToOneDecimal(tx)" class="arrow-red" [icon]="['fas', 'angle-double-down']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
</tbody>
</table>
</div>

View File

@ -0,0 +1,32 @@
.title {
h2 {
line-height: 1;
margin: 0;
padding-bottom: 5px;
}
}
.cpfp-details {
.txids {
width: 60%;
}
@media (max-width: 500px) {
.txids {
width: 40%;
}
}
}
.arrow-green {
color: var(--success);
}
.arrow-red {
color: var(--red);
}
.badge {
position: relative;
top: -1px;
}

View File

@ -0,0 +1,22 @@
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
import { CpfpInfo } from '@interfaces/node-api.interface';
import { Transaction } from '@interfaces/electrs.interface';
@Component({
selector: 'app-cpfp-info',
templateUrl: './cpfp-info.component.html',
styleUrls: ['./cpfp-info.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CpfpInfoComponent implements OnInit {
@Input() cpfpInfo: CpfpInfo;
@Input() tx: Transaction;
constructor() {}
ngOnInit(): void {}
roundToOneDecimal(cpfpTx: any): number {
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
}
}

View File

@ -153,7 +153,7 @@
<ng-template #etaRow>
@if (!isLoadingTx) {
@if (!replaced && !isCached) {
@if (!replaced && !isCached && !unbroadcasted) {
<tr>
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
<td>
@ -170,7 +170,7 @@
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) {
<div class="d-flex accelerate">
<a class="btn btn-sm accelerateDeepMempool btn-small-height" [class.disabled]="!eligibleForAcceleration" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
<a *ngIf="!eligibleForAcceleration" href="https://mempool.space/accelerator#why-cant-accelerate" target="_blank" class="info-badges ml-1" i18n-ngbTooltip="Mempool Accelerator&trade; tooltip" ngbTooltip="This transaction cannot be accelerated">
<a *ngIf="!eligibleForAcceleration" href="https://mempool.space/accelerator#why-cant-accelerate" target="_blank" class="info-badges ml-1" i18n-ngbTooltip="Mempool Accelerator&reg; tooltip" ngbTooltip="This transaction cannot be accelerated">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</div>
@ -184,7 +184,7 @@
</td>
</tr>
}
} @else {
} @else if (!unbroadcasted){
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
</ng-template>
@ -213,11 +213,11 @@
@if (!isLoadingTx) {
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
<td class="text-wrap">{{ (tx.fee | number) ?? '-' }} <span class="symbol" i18n="shared.sats">sats</span>
@if (isAcceleration && accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
}
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + (isAcceleration ? ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0) : 0)"></app-fiat></span>
<span class="fiat"><app-fiat *ngIf="tx.fee >= 0" [blockConversion]="tx.price" [value]="tx.fee + (isAcceleration ? ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0) : 0)"></app-fiat></span>
</td>
</tr>
} @else {
@ -318,4 +318,4 @@
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
</ng-template>

View File

@ -38,6 +38,7 @@ export class TransactionDetailsComponent implements OnInit {
@Input() replaced: boolean;
@Input() isCached: boolean;
@Input() ETA$: Observable<ETA>;
@Input() unbroadcasted: boolean;
@Output() accelerateClicked = new EventEmitter<boolean>();
@Output() toggleCpfp$ = new EventEmitter<void>();

View File

@ -43,6 +43,8 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
opReturns: Vout[];
extraData: 'none' | 'coinbase' | 'opreturn';
ogSession: number;
constructor(
private route: ActivatedRoute,
private electrsApiService: ElectrsApiService,
@ -75,7 +77,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
)
.subscribe((cpfpInfo) => {
this.cpfpInfo = cpfpInfo;
this.openGraphService.waitOver('cpfp-data-' + this.txId);
this.openGraphService.waitOver({ event: 'cpfp-data-' + this.txId, sessionId: this.ogSession });
});
this.subscription = this.route.paramMap
@ -83,8 +85,8 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
switchMap((params: ParamMap) => {
const urlMatch = (params.get('id') || '').split(':');
this.txId = urlMatch[0];
this.openGraphService.waitFor('tx-data-' + this.txId);
this.openGraphService.waitFor('tx-time-' + this.txId);
this.ogSession = this.openGraphService.waitFor('tx-data-' + this.txId);
this.ogSession = this.openGraphService.waitFor('tx-time-' + this.txId);
this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
);
@ -138,7 +140,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
.subscribe((tx: Transaction) => {
if (!tx) {
this.seoService.logSoft404();
this.openGraphService.fail('tx-data-' + this.txId);
this.openGraphService.fail({ event: 'tx-data-' + this.txId, sessionId: this.ogSession });
return;
}
@ -155,10 +157,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
if (tx.status.confirmed) {
this.transactionTime = tx.status.block_time;
this.openGraphService.waitOver('tx-time-' + this.txId);
this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession });
} else if (!tx.status.confirmed && tx.firstSeen) {
this.transactionTime = tx.firstSeen;
this.openGraphService.waitOver('tx-time-' + this.txId);
this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession });
} else {
this.getTransactionTime();
}
@ -184,11 +186,11 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
}
}
this.openGraphService.waitOver('tx-data-' + this.txId);
this.openGraphService.waitOver({ event: 'tx-data-' + this.txId, sessionId: this.ogSession });
},
(error) => {
this.seoService.logSoft404();
this.openGraphService.fail('tx-data-' + this.txId);
this.openGraphService.fail({ event: 'tx-data-' + this.txId, sessionId: this.ogSession });
this.error = error;
this.isLoadingTx = false;
}
@ -205,7 +207,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
)
.subscribe((transactionTimes) => {
this.transactionTime = transactionTimes[0];
this.openGraphService.waitOver('tx-time-' + this.txId);
this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession });
});
}

View File

@ -0,0 +1,215 @@
<div class="container-xl">
@if (!transaction) {
<h1 style="margin-top: 19px;" i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</h1>
<form [formGroup]="pushTxForm" (submit)="decodeTransaction()" novalidate>
<div class="mb-3">
<textarea formControlName="txRaw" class="form-control" rows="5" i18n-placeholder="transaction.hex-and-psbt" placeholder="Transaction hex or base64 encoded PSBT"></textarea>
</div>
<button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.preview|Preview">Preview</button>
<input type="checkbox" [checked]="!offlineMode" id="offline-mode" (change)="onOfflineModeChange($event)">
<label class="label" for="offline-mode">
<span i18n="transaction.fetch-prevout-data">Fetch missing prevouts</span>
</label>
<p *ngIf="error" class="red-color d-inline">Error decoding transaction, reason: {{ error }}</p>
</form>
}
@if (transaction && !error && !isLoading) {
<div class="title-block">
<h1 i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</h1>
<span class="tx-link">
<span class="txid">
<app-truncate [text]="transaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, transaction.txid]" [disabled]="!successBroadcast">
<app-clipboard [text]="transaction.txid"></app-clipboard>
</app-truncate>
</span>
</span>
<div class="container-buttons">
<button class="btn btn-sm" style="margin-left: 10px; padding: 0;" (click)="resetForm()">&#10005;</button>
</div>
</div>
<p class="red-color d-inline">{{ errorBroadcast }}</p>
<div class="clearfix"></div>
<div class="alert alert-mempool" style="align-items: center;">
<span>
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
<ng-container *ngIf="!successBroadcast" i18n="transaction.local-tx|This transaction is stored locally in your browser.">
This transaction is stored locally in your browser. Broadcast it to add it to the mempool.
</ng-container>
<ng-container *ngIf="successBroadcast" i18n="transaction.redirecting|Redirecting to transaction page...">
Redirecting to transaction page...
</ng-container>
</span>
<button *ngIf="!successBroadcast" [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary btn-broadcast" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button>
<button *ngIf="successBroadcast" type="button" class="btn btn-sm btn-success no-cursor btn-broadcast" i18n="transaction.broadcasted|Broadcasted">Broadcasted</button>
</div>
@if (!hasPrevouts) {
<div class="alert alert-mempool">
@if (offlineMode) {
<span><strong>Missing prevouts are not loaded. Some fields like fee rate cannot be calculated.</strong></span>
} @else {
<span><strong>Error loading missing prevouts</strong>. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}</span>
}
</div>
}
@if (errorCpfpInfo) {
<div class="alert alert-mempool">
<span><strong>Error loading CPFP data</strong>. Reason: {{ errorCpfpInfo }}</span>
</div>
}
<app-transaction-details
[unbroadcasted]="true"
[network]="stateService.network"
[tx]="transaction"
[isLoadingTx]="false"
[isMobile]="isMobile"
[isLoadingFirstSeen]="false"
[featuresEnabled]="true"
[filters]="filters"
[hasEffectiveFeeRate]="hasEffectiveFeeRate"
[cpfpInfo]="cpfpInfo"
[hasCpfp]="hasCpfp"
(toggleCpfp$)="this.showCpfpDetails = !this.showCpfpDetails"
></app-transaction-details>
<app-cpfp-info *ngIf="showCpfpDetails" [cpfpInfo]="cpfpInfo" [tx]="transaction"></app-cpfp-info>
<br>
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
<div class="title float-left">
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
</div>
<button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-diagram">Hide diagram</button>
<div class="clearfix"></div>
<div class="box">
<div class="graph-container" #graphContainer>
<tx-bowtie-graph
[tx]="transaction"
[cached]="true"
[width]="graphWidth"
[height]="graphHeight"
[lineLimit]="inOutLimit"
[maxStrands]="graphExpanded ? maxInOut : 24"
[network]="stateService.network"
[tooltip]="true"
[connectors]="true"
[inputIndex]="null" [outputIndex]="null"
>
</tx-bowtie-graph>
</div>
<div class="toggle-wrapper" *ngIf="maxInOut > 24">
<button class="btn btn-sm btn-primary graph-toggle" (click)="expandGraph();" *ngIf="!graphExpanded; else collapseBtn"><span i18n="show-more">Show more</span></button>
<ng-template #collapseBtn>
<button class="btn btn-sm btn-primary graph-toggle" (click)="collapseGraph();"><span i18n="show-less">Show less</span></button>
</ng-template>
</div>
</div>
<br>
</ng-container>
<ng-template #flowPlaceholder>
<div class="box hidden">
<div class="graph-container" #graphContainer>
</div>
</div>
</ng-template>
<div class="subtitle-block">
<div class="title">
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
</div>
<div class="title-buttons">
<button *ngIf="!flowEnabled" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
<button type="button" class="btn btn-outline-info btn-sm" (click)="txList.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
</div>
</div>
<app-transactions-list #txList [transactions]="[transaction]" [transactionPage]="true" [txPreview]="true"></app-transactions-list>
<div class="title text-left">
<h2 i18n="transaction.details|Transaction Details">Details</h2>
</div>
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="block.size">Size</td>
<td [innerHTML]="'&lrm;' + (transaction.size | bytes: 2)"></td>
</tr>
<tr>
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td [innerHTML]="'&lrm;' + (transaction.weight / 4 | vbytes: 2)"></td>
</tr>
<tr *ngIf="adjustedVsize">
<td><ng-container i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</ng-container>
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-is-adjusted-vsize">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</td>
<td [innerHTML]="'&lrm;' + (adjustedVsize | vbytes: 2)"></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (transaction.weight | wuBytes: 2)"></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="transaction.version">Version</td>
<td [innerHTML]="'&lrm;' + (transaction.version | number)"></td>
</tr>
<tr>
<td i18n="transaction.locktime">Locktime</td>
<td [innerHTML]="'&lrm;' + (transaction.locktime | number)"></td>
</tr>
<tr *ngIf="transaction.sigops >= 0">
<td><ng-container i18n="transaction.sigops|Transaction Sigops">Sigops</ng-container>
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-are-sigops">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</td>
<td [innerHTML]="'&lrm;' + (transaction.sigops | number)"></td>
</tr>
<tr>
<td i18n="transaction.hex">Transaction hex</td>
<td><app-clipboard [text]="rawHexTransaction" [leftPadding]="false"></app-clipboard></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
}
@if (isLoading) {
<div class="text-center">
<div class="spinner-border text-light mt-2 mb-2"></div>
<h3 i18n="transaction.error.loading-prevouts">
Loading {{ isLoadingPrevouts ? 'transaction prevouts' : isLoadingCpfpInfo ? 'CPFP' : '' }}
</h3>
</div>
}
</div>

View File

@ -0,0 +1,202 @@
.label {
margin: 0 5px;
}
.container-buttons {
align-self: center;
}
.title-block {
flex-wrap: wrap;
align-items: baseline;
@media (min-width: 650px) {
flex-direction: row;
}
h1 {
margin: 0rem;
margin-right: 15px;
line-height: 1;
}
}
.tx-link {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: baseline;
width: 0;
max-width: 100%;
margin-right: 0px;
margin-bottom: 0px;
margin-top: 8px;
@media (min-width: 651px) {
flex-grow: 1;
margin-bottom: 0px;
margin-right: 1em;
top: 1px;
position: relative;
}
@media (max-width: 650px) {
width: 100%;
order: 3;
}
.txid {
width: 200px;
min-width: 200px;
flex-grow: 1;
}
}
.container-xl {
margin-bottom: 40px;
}
.row {
flex-direction: column;
@media (min-width: 850px) {
flex-direction: row;
}
}
.box.hidden {
visibility: hidden;
height: 0px;
padding-top: 0px;
padding-bottom: 0px;
margin-top: 0px;
margin-bottom: 0px;
}
.graph-container {
position: relative;
width: 100%;
background: var(--stat-box-bg);
padding: 10px 0;
padding-bottom: 0;
}
.toggle-wrapper {
width: 100%;
text-align: center;
margin: 1.25em 0 0;
}
.graph-toggle {
margin: auto;
}
.table {
tr td {
padding: 0.75rem 0.5rem;
@media (min-width: 576px) {
padding: 0.75rem 0.75rem;
}
&:last-child {
text-align: right;
@media (min-width: 850px) {
text-align: left;
}
}
.btn {
display: block;
}
&.wrap-cell {
white-space: normal;
}
}
}
.effective-fee-container {
display: block;
@media (min-width: 768px){
display: inline-block;
}
@media (max-width: 425px){
display: flex;
flex-direction: column;
}
}
.effective-fee-rating {
@media (max-width: 767px){
margin-right: 0px !important;
}
}
.title {
h2 {
line-height: 1;
margin: 0;
padding-bottom: 5px;
}
}
.btn-outline-info {
margin-top: 5px;
@media (min-width: 768px){
margin-top: 0px;
}
}
.flow-toggle {
margin-top: -5px;
margin-left: 10px;
@media (min-width: 768px){
display: inline-block;
margin-top: 0px;
margin-bottom: 0px;
}
}
.subtitle-block {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
.title {
flex-shrink: 0;
}
.title-buttons {
flex-shrink: 1;
text-align: right;
.btn {
margin-top: 0;
margin-bottom: 8px;
margin-left: 8px;
}
}
}
.cpfp-details {
.txids {
width: 60%;
}
@media (max-width: 500px) {
.txids {
width: 40%;
}
}
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
.no-cursor {
cursor: default !important;
pointer-events: none;
}
.btn-broadcast {
margin-left: 5px;
@media (max-width: 567px) {
margin-left: 0;
margin-top: 5px;
}
}

View File

@ -0,0 +1,320 @@
import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { Transaction, Vout } from '@interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
import { Filter, toFilters } from '../../shared/filters.utils';
import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils';
import { catchError, firstValueFrom, Subscription, switchMap, tap, throwError, timer } from 'rxjs';
import { WebsocketService } from '../../services/websocket.service';
import { ActivatedRoute, Router } from '@angular/router';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { ApiService } from '../../services/api.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { CpfpInfo } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-transaction-raw',
templateUrl: './transaction-raw.component.html',
styleUrls: ['./transaction-raw.component.scss'],
})
export class TransactionRawComponent implements OnInit, OnDestroy {
pushTxForm: UntypedFormGroup;
rawHexTransaction: string;
isLoading: boolean;
isLoadingPrevouts: boolean;
isLoadingCpfpInfo: boolean;
offlineMode: boolean = false;
transaction: Transaction;
error: string;
errorPrevouts: string;
errorCpfpInfo: string;
hasPrevouts: boolean;
missingPrevouts: string[];
isLoadingBroadcast: boolean;
errorBroadcast: string;
successBroadcast: boolean;
broadcastSubscription: Subscription;
isMobile: boolean;
@ViewChild('graphContainer')
graphContainer: ElementRef;
graphExpanded: boolean = false;
graphWidth: number = 1068;
graphHeight: number = 360;
inOutLimit: number = 150;
maxInOut: number = 0;
flowPrefSubscription: Subscription;
hideFlow: boolean = this.stateService.hideFlow.value;
flowEnabled: boolean;
adjustedVsize: number;
filters: Filter[] = [];
hasEffectiveFeeRate: boolean;
fetchCpfp: boolean;
cpfpInfo: CpfpInfo | null;
hasCpfp: boolean = false;
showCpfpDetails = false;
mempoolBlocksSubscription: Subscription;
constructor(
public route: ActivatedRoute,
public router: Router,
public stateService: StateService,
public electrsApi: ElectrsApiService,
public websocketService: WebsocketService,
public formBuilder: UntypedFormBuilder,
public seoService: SeoService,
public apiService: ApiService,
public relativeUrlPipe: RelativeUrlPipe,
) {}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@meta.title.preview-tx:Preview Transaction`);
this.seoService.setDescription($localize`:@@meta.description.preview-tx:Preview a transaction to the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network using the transaction's raw hex data.`);
this.websocketService.want(['blocks', 'mempool-blocks']);
this.pushTxForm = this.formBuilder.group({
txRaw: ['', Validators.required],
});
}
async decodeTransaction(): Promise<void> {
this.resetState();
this.isLoading = true;
try {
const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value.trim(), this.stateService.network);
await this.fetchPrevouts(tx);
await this.fetchCpfpInfo(tx);
this.processTransaction(tx, hex);
} catch (error) {
this.error = error.message;
} finally {
this.isLoading = false;
}
}
async fetchPrevouts(transaction: Transaction): Promise<void> {
const prevoutsToFetch = transaction.vin.filter(input => !input.prevout).map((input) => ({ txid: input.txid, vout: input.vout }));
if (!prevoutsToFetch.length || transaction.vin[0].is_coinbase || this.offlineMode) {
this.hasPrevouts = !prevoutsToFetch.length || transaction.vin[0].is_coinbase;
this.fetchCpfp = this.hasPrevouts && !this.offlineMode;
} else {
try {
this.missingPrevouts = [];
this.isLoadingPrevouts = true;
const prevouts: { prevout: Vout, unconfirmed: boolean }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch));
if (prevouts?.length !== prevoutsToFetch.length) {
throw new Error();
}
let fetchIndex = 0;
transaction.vin.forEach(input => {
if (!input.prevout) {
const fetched = prevouts[fetchIndex];
if (fetched) {
input.prevout = fetched.prevout;
} else {
this.missingPrevouts.push(`${input.txid}:${input.vout}`);
}
fetchIndex++;
}
});
if (this.missingPrevouts.length) {
throw new Error(`Some prevouts do not exist or are already spent (${this.missingPrevouts.length})`);
}
this.hasPrevouts = true;
this.isLoadingPrevouts = false;
this.fetchCpfp = prevouts.some(prevout => prevout?.unconfirmed);
} catch (error) {
console.log(error);
this.errorPrevouts = error?.error?.error || error?.message;
this.isLoadingPrevouts = false;
}
}
if (this.hasPrevouts) {
transaction.fee = transaction.vin.some(input => input.is_coinbase)
? 0
: transaction.vin.reduce((fee, input) => {
return fee + (input.prevout?.value || 0);
}, 0) - transaction.vout.reduce((sum, output) => sum + output.value, 0);
transaction.feePerVsize = transaction.fee / (transaction.weight / 4);
}
transaction.vin.forEach(addInnerScriptsToVin);
transaction.sigops = countSigops(transaction);
}
async fetchCpfpInfo(transaction: Transaction): Promise<void> {
// Fetch potential cpfp data if all prevouts were parsed successfully and at least one of them is unconfirmed
if (this.hasPrevouts && this.fetchCpfp) {
try {
this.isLoadingCpfpInfo = true;
const cpfpInfo: CpfpInfo[] = await firstValueFrom(this.apiService.getCpfpLocalTx$([{
txid: transaction.txid,
weight: transaction.weight,
sigops: transaction.sigops,
fee: transaction.fee,
vin: transaction.vin,
vout: transaction.vout
}]));
if (cpfpInfo?.[0]?.ancestors?.length) {
const { ancestors, effectiveFeePerVsize } = cpfpInfo[0];
transaction.effectiveFeePerVsize = effectiveFeePerVsize;
this.cpfpInfo = { ancestors, effectiveFeePerVsize };
this.hasCpfp = true;
this.hasEffectiveFeeRate = true;
}
this.isLoadingCpfpInfo = false;
} catch (error) {
this.errorCpfpInfo = error?.error?.error || error?.message;
this.isLoadingCpfpInfo = false;
}
}
}
processTransaction(tx: Transaction, hex: string): void {
this.transaction = tx;
this.rawHexTransaction = hex;
this.transaction.flags = getTransactionFlags(this.transaction, this.cpfpInfo, null, null, this.stateService.network);
this.filters = this.transaction.flags ? toFilters(this.transaction.flags).filter(f => f.txPage) : [];
if (this.transaction.sigops >= 0) {
this.adjustedVsize = Math.max(this.transaction.weight / 4, this.transaction.sigops * 5);
}
this.setupGraph();
this.setFlowEnabled();
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
this.hideFlow = !!hide;
this.setFlowEnabled();
});
this.setGraphSize();
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe(() => {
if (this.transaction) {
this.stateService.markBlock$.next({
txid: this.transaction.txid,
txFeePerVSize: this.transaction.effectiveFeePerVsize || this.transaction.feePerVsize,
});
}
});
}
postTx(): void {
this.isLoadingBroadcast = true;
this.errorBroadcast = null;
this.broadcastSubscription = this.apiService.postTransaction$(this.rawHexTransaction).pipe(
tap((txid: string) => {
this.isLoadingBroadcast = false;
this.successBroadcast = true;
this.transaction.txid = txid;
}),
switchMap((txid: string) =>
timer(2000).pipe(
tap(() => this.router.navigate([this.relativeUrlPipe.transform('/tx/' + txid)])),
)
),
catchError((error) => {
if (typeof error.error === 'string') {
const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"');
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error);
} else if (error.message) {
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + error.message;
}
this.isLoadingBroadcast = false;
return throwError(() => error);
})
).subscribe();
}
resetState() {
this.transaction = null;
this.rawHexTransaction = null;
this.error = null;
this.errorPrevouts = null;
this.errorBroadcast = null;
this.successBroadcast = false;
this.isLoading = false;
this.isLoadingPrevouts = false;
this.isLoadingCpfpInfo = false;
this.isLoadingBroadcast = false;
this.adjustedVsize = null;
this.showCpfpDetails = false;
this.hasCpfp = false;
this.fetchCpfp = false;
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
this.filters = [];
this.hasPrevouts = false;
this.missingPrevouts = [];
this.stateService.markBlock$.next({});
this.mempoolBlocksSubscription?.unsubscribe();
this.broadcastSubscription?.unsubscribe();
}
resetForm() {
this.resetState();
this.pushTxForm.get('txRaw').setValue('');
}
@HostListener('window:resize', ['$event'])
setGraphSize(): void {
this.isMobile = window.innerWidth < 850;
if (this.graphContainer?.nativeElement && this.stateService.isBrowser) {
setTimeout(() => {
if (this.graphContainer?.nativeElement?.clientWidth) {
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
} else {
setTimeout(() => { this.setGraphSize(); }, 1);
}
}, 1);
} else {
setTimeout(() => { this.setGraphSize(); }, 1);
}
}
setupGraph() {
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.transaction?.vin?.length || 1, this.transaction?.vout?.length + 1 || 1));
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
}
toggleGraph() {
const showFlow = !this.flowEnabled;
this.stateService.hideFlow.next(!showFlow);
}
setFlowEnabled() {
this.flowEnabled = !this.hideFlow;
}
expandGraph() {
this.graphExpanded = true;
this.graphHeight = this.maxInOut * 15;
}
collapseGraph() {
this.graphExpanded = false;
this.graphHeight = Math.min(360, this.maxInOut * 80);
}
onOfflineModeChange(e): void {
this.offlineMode = !e.target.checked;
}
ngOnDestroy(): void {
this.mempoolBlocksSubscription?.unsubscribe();
this.flowPrefSubscription?.unsubscribe();
this.stateService.markBlock$.next({});
this.broadcastSubscription?.unsubscribe();
}
}

View File

@ -67,64 +67,7 @@
<ng-template [ngIf]="!isLoadingTx && !error">
<!-- CPFP Details -->
<ng-template [ngIf]="showCpfpDetails">
<br>
<div class="title">
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
</div>
<div class="box cpfp-details">
<table class="table table-fixed table-borderless table-striped">
<thead>
<tr>
<th i18n="transactions-list.vout.scriptpubkey-type">Type</th>
<th class="txids" i18n="dashboard.latest-transactions.txid">TXID</th>
<th *only-vsize class="d-none d-lg-table-cell" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</th>
<th *only-weight class="d-none d-lg-table-cell" i18n="transaction.weight|Transaction Weight">Weight</th>
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
<th class="d-none d-lg-table-cell"></th>
</tr>
</thead>
<tbody>
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td>
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) > roundToOneDecimal(tx)" class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
<tr>
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td class="txids">
<app-truncate [text]="cpfpInfo.bestDescendant.txid" [link]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpInfo.bestDescendant.fee" [weight]="cpfpInfo.bestDescendant.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
<td class="txids">
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) < roundToOneDecimal(tx)" class="arrow-red" [icon]="['fas', 'angle-double-down']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
</tbody>
</table>
</div>
</ng-template>
<app-cpfp-info *ngIf="showCpfpDetails" [cpfpInfo]="cpfpInfo" [tx]="tx"></app-cpfp-info>
<!-- Accelerator -->
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary && (ETA$ | async) as eta;">

View File

@ -227,18 +227,6 @@
}
}
.cpfp-details {
.txids {
width: 60%;
}
@media (max-width: 500px) {
.txids {
width: 40%;
}
}
}
.tx-list {
.alert-link {
display: block;

View File

@ -1049,10 +1049,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.stateService.markBlock$.next({});
}
roundToOneDecimal(cpfpTx: any): number {
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
}
setupGraph() {
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);

View File

@ -9,6 +9,8 @@ import { TransactionExtrasModule } from '@components/transaction/transaction-ext
import { GraphsModule } from '@app/graphs/graphs.module';
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
import { AccelerateFeeGraphComponent } from '@components/accelerate-checkout/accelerate-fee-graph.component';
import { TransactionRawComponent } from '@components/transaction/transaction-raw.component';
import { CpfpInfoComponent } from '@components/transaction/cpfp-info.component';
const routes: Routes = [
{
@ -16,6 +18,10 @@ const routes: Routes = [
redirectTo: '/',
pathMatch: 'full',
},
{
path: 'preview',
component: TransactionRawComponent,
},
{
path: ':id',
component: TransactionComponent,
@ -49,12 +55,15 @@ export class TransactionRoutingModule { }
TransactionDetailsComponent,
AccelerateCheckout,
AccelerateFeeGraphComponent,
TransactionRawComponent,
CpfpInfoComponent,
],
exports: [
TransactionComponent,
TransactionDetailsComponent,
AccelerateCheckout,
AccelerateFeeGraphComponent,
CpfpInfoComponent,
]
})
export class TransactionModule { }

View File

@ -16,6 +16,11 @@
<div class="header-bg box" [attr.data-cy]="'tx-' + i">
<div *ngIf="errorUnblinded" class="error-unblinded">{{ errorUnblinded }}</div>
@if (similarityMatches.get(tx.txid)?.size) {
<div class="alert alert-mempool" role="alert">
<span i18n="transaction.poison.warning">Warning! This transaction involves deceptively similar addresses. It may be an address poisoning attack.</span>
</div>
}
<div class="row">
<div class="col">
<table class="table table-fixed table-borderless smaller-text table-sm table-tx-vin">
@ -68,9 +73,11 @@
</ng-template>
</ng-template>
<ng-template #defaultAddress>
<a class="address" *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<app-truncate [text]="vin.prevout.scriptpubkey_address" [lastChars]="8"></app-truncate>
</a>
<app-address-text
*ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType"
[address]="vin.prevout.scriptpubkey_address"
[similarity]="similarityMatches.get(tx.txid)?.get(vin.prevout.scriptpubkey_address)"
></app-address-text>
<ng-template #vinScriptPubkeyType>
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
</ng-template>
@ -216,9 +223,11 @@
'highlight': this.addresses.length && ((vout.scriptpubkey_type !== 'p2pk' && addresses.includes(vout.scriptpubkey_address)) || this.addresses.includes(vout.scriptpubkey.slice(2, -2)))
}">
<td class="address-cell">
<a class="address" *ngIf="vout.scriptpubkey_address; else pubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
</a>
<app-address-text
*ngIf="vout.scriptpubkey_address; else pubkey_type"
[address]="vout.scriptpubkey_address"
[similarity]="similarityMatches.get(tx.txid)?.get(vout.scriptpubkey_address)"
></app-address-text>
<ng-template #pubkey_type>
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, -2)]" title="{{ vout.scriptpubkey.slice(2, -2) }}">

View File

@ -14,6 +14,7 @@ import { StorageService } from '@app/services/storage.service';
import { OrdApiService } from '@app/services/ord-api.service';
import { Inscription } from '@app/shared/ord/inscription.utils';
import { Etching, Runestone } from '@app/shared/ord/rune.utils';
import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils';
@Component({
selector: 'app-transactions-list',
@ -37,6 +38,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() addresses: string[] = [];
@Input() rowLimit = 12;
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
@Input() txPreview = false;
@Output() loadMore = new EventEmitter();
@ -54,6 +56,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
showFullScript: { [vinIndex: number]: boolean } = {};
showFullWitness: { [vinIndex: number]: { [witnessIndex: number]: boolean } } = {};
showOrdData: { [key: string]: { show: boolean; inscriptions?: Inscription[]; runestone?: Runestone, runeInfo?: { [id: string]: { etching: Etching; txid: string; } }; } } = {};
similarityMatches: Map<string, Map<string, { score: number, match: AddressMatch, group: number }>> = new Map();
constructor(
public stateService: StateService,
@ -81,7 +84,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.refreshOutspends$
.pipe(
switchMap((txIds) => {
if (!this.cached) {
if (!this.cached && !this.txPreview) {
// break list into batches of 50 (maximum supported by esplora)
const batches = [];
for (let i = 0; i < txIds.length; i += 50) {
@ -119,7 +122,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
),
this.refreshChannels$
.pipe(
filter(() => this.stateService.networkSupportsLightning()),
filter(() => this.stateService.networkSupportsLightning() && !this.txPreview),
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
catchError((error) => {
// handle 404
@ -143,6 +146,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.currency = currency;
this.refreshPrice();
});
this.updateAddressSimilarities();
}
refreshPrice(): void {
@ -182,12 +187,17 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
}
if (changes.transactions || changes.addresses) {
this.similarityMatches.clear();
this.updateAddressSimilarities();
if (!this.transactions || !this.transactions.length) {
return;
}
this.transactionsLength = this.transactions.length;
this.cacheService.setTxCache(this.transactions);
if (!this.txPreview) {
this.cacheService.setTxCache(this.transactions);
}
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
this.transactions.forEach((tx) => {
@ -292,6 +302,56 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
}
updateAddressSimilarities(): void {
if (!this.transactions || !this.transactions.length) {
return;
}
for (const tx of this.transactions) {
if (this.similarityMatches.get(tx.txid)) {
continue;
}
const similarityGroups: Map<string, number> = new Map();
let lastGroup = 0;
// Check for address poisoning similarity matches
this.similarityMatches.set(tx.txid, new Map());
const comparableVouts = [
...tx.vout.slice(0, 20),
...this.addresses.map(addr => ({ scriptpubkey_address: addr, scriptpubkey_type: detectAddressType(addr, this.stateService.network) }))
].filter(v => ['p2pkh', 'p2sh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(v.scriptpubkey_type));
const comparableVins = tx.vin.slice(0, 20).map(v => v.prevout).filter(v => ['p2pkh', 'p2sh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(v?.scriptpubkey_type));
for (const vout of comparableVouts) {
const address = vout.scriptpubkey_address;
const addressType = vout.scriptpubkey_type;
if (this.similarityMatches.get(tx.txid)?.has(address)) {
continue;
}
for (const compareAddr of [
...comparableVouts.filter(v => v.scriptpubkey_type === addressType && v.scriptpubkey_address !== address),
...comparableVins.filter(v => v.scriptpubkey_type === addressType && v.scriptpubkey_address !== address)
]) {
const similarity = checkedCompareAddressStrings(address, compareAddr.scriptpubkey_address, addressType as AddressType, this.stateService.network);
if (similarity?.status === 'comparable' && similarity.score > ADDRESS_SIMILARITY_THRESHOLD) {
let group = similarityGroups.get(address) || lastGroup++;
similarityGroups.set(address, group);
const bestVout = this.similarityMatches.get(tx.txid)?.get(address);
if (!bestVout || bestVout.score < similarity.score) {
this.similarityMatches.get(tx.txid)?.set(address, { score: similarity.score, match: similarity.left, group });
}
// opportunistically update the entry for the compared address
const bestCompare = this.similarityMatches.get(tx.txid)?.get(compareAddr.scriptpubkey_address);
if (!bestCompare || bestCompare.score < similarity.score) {
group = similarityGroups.get(compareAddr.scriptpubkey_address) || lastGroup++;
similarityGroups.set(compareAddr.scriptpubkey_address, group);
this.similarityMatches.get(tx.txid)?.set(compareAddr.scriptpubkey_address, { score: similarity.score, match: similarity.right, group });
}
}
}
}
}
}
onScroll(): void {
this.loadMore.emit();
}
@ -351,7 +411,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
loadMoreInputs(tx: Transaction): void {
if (!tx['@vinLoaded']) {
if (!tx['@vinLoaded'] && !this.txPreview) {
this.electrsApiService.getTransaction$(tx.txid)
.subscribe((newTx) => {
tx['@vinLoaded'] = true;

View File

@ -34,29 +34,39 @@ export class TwitterWidgetComponent implements OnChanges {
}
setIframeSrc(): void {
if (this.handle) {
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL,
`https://syndication.x.com/srv/timeline-profile/screen-name/${this.handle}?creatorScreenName=mempool`
+ '&dnt=true'
+ '&embedId=twitter-widget-0'
+ '&features=eyJ0ZndfdGltZWxpbmVfgbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D'
+ '&frame=false'
+ '&hideBorder=true'
+ '&hideFooter=false'
+ '&hideHeader=true'
+ '&hideScrollBar=false'
+ `&lang=${this.lang}`
+ '&maxHeight=500px'
+ '&origin=https%3A%2F%2Fmempool.space%2F'
// + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9'
+ '&showHeader=false'
+ '&showReplies=false'
+ '&siteScreenName=mempool'
+ '&theme=dark'
+ '&transparent=true'
+ '&widgetsVersion=2615f7e52b7e0%3A1702314776716'
));
if (!this.handle) {
return;
}
let url = `https://syndication.x.com/srv/timeline-profile/screen-name/${this.handle}?creatorScreenName=mempool`
+ '&dnt=true'
+ '&embedId=twitter-widget-0'
+ '&features=eyJ0ZndfdGltZWxpbmVfgbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D'
+ '&frame=false'
+ '&hideBorder=true'
+ '&hideFooter=false'
+ '&hideHeader=true'
+ '&hideScrollBar=false'
+ `&lang=${this.lang}`
+ '&maxHeight=500px'
+ '&origin=https%3A%2F%2Fmempool.space%2F'
// + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9'
+ '&showHeader=false'
+ '&showReplies=false'
+ '&siteScreenName=mempool'
+ '&theme=dark'
+ '&transparent=true'
+ '&widgetsVersion=2615f7e52b7e0%3A1702314776716';
switch (this.handle.toLowerCase()) {
case 'nayibbukele':
url = 'https://bitcoin.gob.sv/twidget';
break;
case 'metaplanet_jp':
url = 'https://metaplanet.mempool.space/twidget';
break;
default:
break;
}
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, url));
}
onReady(): void {

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