Compare commits

..

1388 Commits

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-29 02:07:00 +00:00
softsimon
558b876d47 pull from transifex 2024-07-28 19:48:15 -05:00
TechMiX
fb63af5070 fix various rtl issues 2024-07-29 02:17:09 +02:00
wiz
d02a2ccf65 Merge pull request #5380 from mempool/simon/1w-1m
fixes crash with missing pool partner
2024-07-28 14:40:50 -05:00
softsimon
e3bb812203 fixes crash with missing pool partner
fixes #5379
2024-07-28 14:10:50 -05:00
softsimon
e3af4ea61c pull from transifex 2024-07-28 13:20:58 -05:00
softsimon
7d10b32861 pull from transifex 2024-07-26 16:43:17 -05:00
wiz
adea897e93 Merge branch 'master' into mononaut/tracker-tx-routing 2024-07-26 11:29:31 -05:00
wiz
17ec50dba0 Merge pull request #5371 from mempool/natsoni/timeline-tooltip
Add tooltip to acceleration timeline
2024-07-26 11:12:15 -05:00
Mononaut
53a36d042f Fix /tx redirect merge conflicts 2024-07-26 16:11:59 +00:00
softsimon
7531a53b2e correct i18n 2024-07-26 11:06:36 -05:00
Mononaut
d59bc085e5 Redirect direct mobile tx visits to pizza tracker 2024-07-26 15:54:22 +00:00
wiz
5d37e08c64 Merge branch 'master' into natsoni/timeline-tooltip 2024-07-26 10:44:01 -05:00
wiz
b5d89b83fa Merge pull request #5368 from mempool/nymkappa/google-pay
[accelerator] add support for Google Pay payment
2024-07-26 10:43:50 -05:00
nymkappa
1245673575 [accelerator] polish UI 2024-07-26 17:39:58 +02:00
softsimon
62a72755c7 pull from transifex 2024-07-26 10:25:29 -05:00
wiz
5b8ec8925a Merge pull request #5373 from mempool/mononaut/no-405
[accelerator] remove dumb log request 405 response
2024-07-26 10:22:38 -05:00
softsimon
262866c15b Merge pull request #5369 from mempool/nymkappa/getjwtemptynonofficial
[services] getJWT call returns nothing if non official
2024-07-26 06:36:21 -05:00
softsimon
aec7eb57c2 Merge pull request #5370 from mempool/natsoni/fix-loading-spinner
Fix loading spinner z-index
2024-07-26 05:10:25 -05:00
Mononaut
8d3b4733f5 [accelerator] remove dumb 405 log request response 2024-07-26 09:49:42 +00:00
nymkappa
3e4debdf7a Merge branch 'master' into nymkappa/google-pay 2024-07-26 11:38:47 +02:00
nymkappa
b719b76999 Merge pull request #5372 from mempool/mononaut/google-play-minor-fixes
minor accelerator checkout fixes
2024-07-26 11:37:54 +02:00
Mononaut
6adbda5185 Fix accelerator checkout linting & type errors 2024-07-26 09:27:18 +00:00
natsoni
01311d0ba1 Add tooltip to timeline 2024-07-26 11:21:24 +02:00
Mononaut
6081daacef [accelerator] add missing getters etc 2024-07-26 09:06:47 +00:00
natsoni
54c9970386 Merge branch 'master' into natsoni/timeline-tooltip 2024-07-26 11:04:17 +02:00
softsimon
1e4a599055 fix accelerations row height 2024-07-25 21:05:32 -05:00
softsimon
b051861d61 Merge pull request #5365 from mempool/natsoni/fix-mining-data
Get all pools in accelerations list
2024-07-25 21:03:52 -05:00
natsoni
3c7deafffd Fix loading spinner z-index 2024-07-26 00:00:14 +02:00
nymkappa
845123fc63 [services] getJWT call returns nothing if non official 2024-07-25 23:04:53 +02:00
nymkappa
d3e3650cac [accelerator] avoid premature square setup call 2024-07-25 21:49:48 +02:00
softsimon
008cc385da pull from transifex 2024-07-25 13:01:37 -05:00
softsimon
817a6bef6e Merge pull request #5367 from mempool/knorrium/change_default_var
Set the default value to the SERVICES_API variable
2024-07-25 12:59:40 -05:00
Felipe Knorr Kuhn
8dd8fe5fb1 Fix string value 2024-07-25 12:57:36 -05:00
softsimon
c734a81f08 pull from transifex 2024-07-25 12:27:32 -05:00
Felipe Knorr Kuhn
a0f4c260ab Set the default URL for the backend 2024-07-25 12:20:01 -05:00
Felipe Knorr Kuhn
000a989055 Set the default value to the SERVICES_API variable 2024-07-25 11:29:17 -05:00
natsoni
90331e2c1b Get all pools in accelerations list 2024-07-25 17:45:08 +02:00
wiz
78ac0137b3 Merge pull request #5350 from mempool/orangesurf/2024-07-19
Update webserver line
2024-07-25 10:21:53 -05:00
natsoni
3d9133c47e Add fee delta to acceleration data 2024-07-25 16:52:10 +02:00
nymkappa
481859bc8f [accelerator] add support for Google Pay payment 2024-07-25 15:54:24 +02:00
orangesurf
811feec145 Merge branch 'master' into orangesurf/2024-07-19 2024-07-25 18:47:27 +09:00
softsimon
b3e59c06e9 add new accelerator button config to sample 2024-07-25 04:07:18 -05:00
softsimon
67d44e3d6f Merge pull request #5359 from mempool/natsoni/fix-loop-calling-transactionTimes
Prevent never ending loop of calls to transactionTimes
2024-07-25 03:38:06 -05:00
softsimon
ca4b1943a8 moving code block 2024-07-25 03:35:17 -05:00
natsoni
df0f244bd1 Prevent never ending loop of calls to transactionTimes 2024-07-25 00:53:13 +02:00
softsimon
fe1ad86885 Merge pull request #5362 from mempool/natsoni/more-data-acc-table
Add fee delta and pool name to acceleration list
2024-07-24 17:42:28 -05:00
natsoni
aee2454a98 Add pool name to acceleration list 2024-07-25 00:24:08 +02:00
softsimon
5c814d9c22 pending balance -> pending 2024-07-24 17:21:55 -05:00
wiz
2c5d7fbc9f Merge pull request #5363 from mempool/nymkappa/apple-pay-hotfix
[accelerator] hide fiat payment method section if none available
2024-07-24 17:11:01 -05:00
nymkappa
570f7841ce [accelerator] hide fiat payment method section if none available 2024-07-25 00:10:27 +02:00
wiz
117c066425 Merge pull request #5353 from mempool/nymkappa/apple-pay
[accelerator] add support for acceleration with apple pay
2024-07-24 17:01:23 -05:00
softsimon
96f9f66e7f changing unconfirmed to pending balance/utxo 2024-07-24 16:56:30 -05:00
wiz
ce2742ff9c Merge branch 'master' into nymkappa/apple-pay 2024-07-24 16:47:57 -05:00
softsimon
4547a2757c Merge pull request #5351 from mempool/natsoni/fix-recursion-search
Fix recursion loop in search bar
2024-07-24 15:42:51 -05:00
nymkappa
4d44ee55fc [accelerator] add missing getters for applepay 2024-07-24 22:20:52 +02:00
nymkappa
29875e0095 Merge branch 'master' into nymkappa/apple-pay 2024-07-24 22:04:44 +02:00
softsimon
bc498733fc Merge pull request #5355 from mempool/natsoni/fix-blockchain-scroll
Fix unwanted blockchain scroll on screen resize
2024-07-24 11:44:36 -07:00
wiz
dc09e75783 Merge pull request #5361 from mempool/mononaut/fosscelerator
On-demand acceleration polling
2024-07-24 13:44:04 -05:00
wiz
544261eafe ops: Add /api/v1/accelerations to nginx hot cache 2024-07-24 13:31:56 -05:00
softsimon
58c0c060d5 Merge pull request #5358 from mempool/natsoni/fix-miner-loading
Fix miner loading forever
2024-07-24 11:25:48 -07:00
Mononaut
af7a962a0b [accelerator] accelerator_button config 2024-07-24 17:32:44 +00:00
Mononaut
7b3cc6372b [accelerator] frontend on-demand polling support 2024-07-24 17:32:44 +00:00
Mononaut
b49a6c4cac [accelerator] on-demand polling support 2024-07-24 17:32:43 +00:00
wiz
b0db348605 Merge pull request #5357 from mempool/nymkappa/update-api-key-header
[doc] update api key header
2024-07-24 12:27:03 -05:00
wiz
b1aa4f50bd Change X-Mempool-Authorization to X-Mempool-Auth 2024-07-24 12:25:35 -05:00
orangesurf
301f1821ae Merge branch 'master' into orangesurf/2024-07-19 2024-07-23 20:48:03 +09:00
natsoni
b9a053387f Add txConfirmed subscription variable to fix miner loading forever 2024-07-23 11:44:50 +02:00
softsimon
82c271267a extracting i18n's 2024-07-23 00:28:32 +08:00
softsimon
06affa60cc Merge pull request #5352 from mempool/simon/replace-crypto-uuid-func
Fix crypto lib call crash with custom function
2024-07-22 22:38:05 +08:00
nymkappa
4ef4e5b98a [doc] update api key header 2024-07-22 15:17:56 +02:00
wiz
6bc52dcc82 Merge pull request #5356 from mempool/simon/fix-payment-waiting-retry
fix btc payment waiting reply
2024-07-22 21:16:00 +09:00
softsimon
1f357408ac fix btc payment waiting reply 2024-07-22 20:13:13 +08:00
natsoni
a7be59df3e Fix buggy blockchain scroll on resize 2024-07-22 13:36:36 +02:00
orangesurf
82360f5525 Merge branch 'master' into orangesurf/2024-07-19 2024-07-22 17:08:47 +09:00
nymkappa
8762ccaa09 [accelerator] remove attempt to align fiat payment methods 2024-07-21 23:22:11 +02:00
nymkappa
3f7a24fb52 [accelerator] only show apple pay if available 2024-07-21 23:08:08 +02:00
nymkappa
09b09710e4 [accelerator] fix cashapp acceleration on mobile 2024-07-21 23:07:55 +02:00
nymkappa
08d3beed72 [accelerator] on mobile, autoscroll after selection cashapp or applepay 2024-07-21 22:38:49 +02:00
nymkappa
920f225e6c [accelerator] add support for acceleration with apple pay 2024-07-21 22:17:47 +02:00
softsimon
9c2d010516 rename to insecureRandomUUID 2024-07-21 23:55:28 +08:00
softsimon
743c7e8bfb Fix crypto lib call crash with custom function 2024-07-21 22:54:58 +08:00
softsimon
8116b50d50 fix spelling error 2024-07-21 20:34:20 +08:00
softsimon
df977926e2 Merge pull request #5349 from mempool/simon/add-fee-to-cpfp
Add fee to Cpfp API
2024-07-21 19:05:55 +08:00
softsimon
b0fac806d0 Merge pull request #4905 from mempool/mononaut/mini-miner-cpfp
Mini miner cpfp
2024-07-21 19:05:35 +08:00
Mononaut
398593828f Implement CPFP reindexing using mini-miner method (not activated) 2024-07-20 12:08:52 +00:00
Mononaut
9aac0ddce7 Fix merge conflict 2024-07-20 12:08:52 +00:00
Mononaut
79eb9635c2 Fix cpfp vsize rounding & goggles bugs 2024-07-20 12:08:52 +00:00
Mononaut
41c373c39d Mini-miner based block cpfp calculations 2024-07-20 12:08:52 +00:00
Mononaut
27374bd131 Refactor cpfp & single-block gbt code into mini-miner module 2024-07-20 12:08:52 +00:00
natsoni
8c07e3c31a Fix recursion loop in search bar 2024-07-19 16:12:54 +02:00
orangesurf
84ba721407 Update webserver line 2024-07-19 11:48:47 +02:00
softsimon
cdaf42797f Add fee to Cpfp API 2024-07-18 16:44:00 +08:00
wiz
0a116804e8 Merge pull request #5347 from mempool/junderw/fix-docker-aarch64
Bump Rust version to 1.79
2024-07-18 15:08:22 +09:00
junderw
68edf4306c Bump Rust version to 1.79
Maintaining an old MSRV is not a priority for this project.
If you would like to keep an old MSRV active, please maintain your own patch/fork.
2024-07-18 01:07:05 +09:00
softsimon
61bbb95819 fix default docker unix socket path variable 2024-07-17 12:34:28 +08:00
softsimon
3fa32edf25 Merge pull request #5345 from mempool/dependabot/npm_and_yarn/frontend/fortawesome/fontawesome-common-types-6.6.0
Bump @fortawesome/fontawesome-common-types from 6.5.1 to 6.6.0 in /frontend
2024-07-17 11:48:05 +09:00
dependabot[bot]
db220d9dfd Bump @fortawesome/fontawesome-common-types in /frontend
Bumps [@fortawesome/fontawesome-common-types](https://github.com/FortAwesome/Font-Awesome) from 6.5.1 to 6.6.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.5.1...6.6.0)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 02:17:00 +00:00
softsimon
a0d3afb4d2 Merge pull request #5124 from mempool/natsoni/fix-lightning-search
Searchbar: wait for 3 characters before requesting lightning data
2024-06-01 14:22:19 +07:00
softsimon
67afda7dcf Merge branch 'master' into natsoni/fix-lightning-search 2024-06-01 14:20:00 +07:00
softsimon
a56af00500 Merge pull request #5123 from mempool/natsoni/search-results-ordering
Improve search results ordering
2024-06-01 14:19:48 +07:00
softsimon
e3971af207 Merge pull request #5122 from mempool/natsoni/fix-pool-ranking
Fix pool ranking table
2024-06-01 14:17:50 +07:00
Mononaut
37725bb341 Make address graph prefer "recent" mode by default 2024-05-31 17:20:07 +00:00
natsoni
f17635193a Fix pool ranking component update 2024-05-31 17:25:36 +02:00
softsimon
1c73dc59f9 Merge branch 'master' into natsoni/search-results-ordering 2024-05-31 22:18:43 +07:00
softsimon
3adbba2959 Merge branch 'master' into natsoni/fix-lightning-search 2024-05-31 21:20:31 +07:00
softsimon
ea1629fba8 Merge pull request #5121 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.10.0
Bump mysql2 from 3.9.7 to 3.10.0 in /backend
2024-05-31 21:20:02 +07:00
softsimon
87a4c087e5 Merge pull request #5118 from mempool/natsoni/fix-pool-page-update
Fix pool page update
2024-05-31 21:19:35 +07:00
softsimon
692edea1ce Merge branch 'master' into natsoni/fix-pool-page-update 2024-05-31 21:17:09 +07:00
softsimon
11cfb8a783 Merge pull request #5117 from mempool/natsoni/pools-search
Add mining pools to search results
2024-05-31 21:16:46 +07:00
natsoni
0b953f21b0 Only query lightning search if more than 3 characters 2024-05-31 15:40:27 +02:00
natsoni
d5508872dd Select lightning node by default in search results of public key 2024-05-31 15:08:58 +02:00
natsoni
321181d708 Update search results ordering 2024-05-31 13:52:37 +02:00
natsoni
f3bd50d4ab Revert "Update search results ordering"
This reverts commit 00838ea947.
2024-05-31 13:37:30 +02:00
dependabot[bot]
12a843c386 Bump mysql2 from 3.9.7 to 3.10.0 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.9.7 to 3.10.0.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.9.7...v3.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-31 02:34:09 +00:00
natsoni
21f91bcb6e Fix pool page update on slug change 2024-05-30 17:58:28 +02:00
natsoni
d57bd56743 Use includes() instead of startsWith() to search for pool names 2024-05-30 15:08:20 +02:00
natsoni
08969592ea Fix i18n for unknown pool search 2024-05-30 14:46:48 +02:00
wiz
f0437886ee Merge pull request #5116 from mempool/simon/fix-undefined-group-channels 2024-05-30 18:29:53 +09:00
softsimon
cfedb5fd24 Fix for undefined LN group channels 2024-05-30 16:15:51 +07:00
wiz
a9ad892495 Merge pull request #5112 from mempool/mononaut/polish-acc-pie
Polish acceleration pie chart section
2024-05-30 17:58:58 +09:00
natsoni
00838ea947 Update search results ordering 2024-05-30 10:34:40 +02:00
natsoni
7761ea53c6 Add mining pools to search bar 2024-05-30 09:31:44 +02:00
softsimon
aeeb4af9ba Merge pull request #5110 from mempool/natsoni/lift-up-blockchain-toggle
Slightly lift up blockchain toggle button
2024-05-29 16:21:45 +07:00
softsimon
9186f664da Merge pull request #5109 from mempool/natsoni/fix-mining-graphs
Fix widget mining graphs
2024-05-29 15:58:56 +07:00
softsimon
83db2a3b72 Add margin to graph on pool ranking page 2024-05-29 15:58:39 +07:00
natsoni
3cfd54b4c5 Update mining dashboard graph heights 2024-05-29 10:27:45 +02:00
Mononaut
c6db016c99 Show hashrate pie chart immediately on acceleration 2024-05-28 21:33:09 +00:00
Mononaut
6f6a9ea1a4 Brighter purple pie chart 2024-05-28 21:07:36 +00:00
Mononaut
83246be962 Responsive active acceleration details 2024-05-28 21:06:58 +00:00
natsoni
dcd94d868a Slightly lift up blockchain toggle button 2024-05-28 16:11:48 +02:00
natsoni
e9fc5c0433 Fix widget mining graphs 2024-05-28 16:11:06 +02:00
wiz
e281684ca4 Merge pull request #5107 from mempool/mononaut/acceleration-piechart-hotfix
Hotfix for acceleration pie chart section logic
2024-05-28 12:37:22 +09:00
Mononaut
6a915c0b88 Hotfix for acceleration pie chart section logic 2024-05-28 03:35:41 +00:00
wiz
078dc8d9a1 Merge pull request #5090 from mempool/mononaut/update-onbtc-preview-img
Update onbtc preview fallback image
2024-05-28 11:26:33 +09:00
wiz
232f81b906 Merge pull request #5017 from mempool/nymkappa/image-md5
[account] update profile image md5
2024-05-28 11:25:55 +09:00
wiz
8701119304 Merge pull request #5101 from mempool/natsoni/block-rewards-graph
Fees vs subsidy graph: add percentage mode
2024-05-28 11:23:57 +09:00
wiz
33c9f4a8dc Merge pull request #5103 from mempool/mononaut/multi-pool-acc
inline acceleration hashrate pie chart
2024-05-28 11:23:25 +09:00
natsoni
0654872627 Fix graph legend update while load bug and remove unnecessary query 2024-05-27 16:49:29 +02:00
natsoni
cca798eeaa Remove unnecessary filters in graph 2024-05-27 16:42:17 +02:00
Mononaut
1498db3b33 Backend support for multi-pool acceleration details 2024-05-26 20:47:36 +00:00
Mononaut
05b022dec8 multi-pool active accelerating details component 2024-05-26 20:39:35 +00:00
natsoni
6c6c18830c Fees vs subsidy graph: add percentage mode 2024-05-25 12:32:38 +02:00
softsimon
46b5b26347 Merge pull request #5099 from mempool/natsoni/graphs-cleanup
Graphs loading indicators update
2024-05-25 12:36:40 +07:00
natsoni
bd5abf6592 Re-enable graph timespan controls while isLoading 2024-05-24 17:08:36 +02:00
natsoni
2c04896397 Graphs loading indicators update 2024-05-24 14:02:24 +02:00
softsimon
286fc8e9ad Merge pull request #5042 from mempool/natsoni/statistics-replication
Add statistics to replication service
2024-05-24 14:35:30 +07:00
softsimon
52fa6a0f78 Merge pull request #5095 from mempool/mononaut/fix-local-acc-history
Fix local acceleration filter & reindex
2024-05-23 23:13:19 +07:00
softsimon
a53b587395 Merge pull request #5098 from mempool/natsoni/add-pool-health-check
Add check for pool addresses and regexes
2024-05-23 23:03:11 +07:00
softsimon
2341b1d79e Merge pull request #5091 from mempool/mononaut/faucet-ui-changes
Requested changes to testnet4 faucet UI
2024-05-23 19:11:15 +07:00
natsoni
35215c7740 Add check for pool addresses and regexes 2024-05-23 11:30:30 +02:00
softsimon
e2080e5548 Merge pull request #5077 from mempool/dependabot/npm_and_yarn/frontend/frontend-angular-dependencies-ced88fd6cd
Bump ngx-echarts from 17.1.0 to 17.2.0 in /frontend in the frontend-angular-dependencies group
2024-05-23 01:23:08 +07:00
natsoni
60a07fb093 Improve statistics replication service 2024-05-22 18:10:52 +02:00
softsimon
28477cc433 Merge pull request #5085 from mempool/simon/refactor-block-page
Refactor block transactions
2024-05-22 22:36:48 +07:00
Mononaut
6bb2f06118 Faucet: clamp default amount to minimum 2024-05-22 15:28:15 +00:00
softsimon
68bab5b7b8 Merge pull request #5097 from mempool/dependabot/npm_and_yarn/backend/axios-1.7.2
Bump axios from 1.6.1 to 1.7.2 in /backend
2024-05-22 17:19:43 +07:00
dependabot[bot]
6bc7eec4e3 Bump ngx-echarts in /frontend in the frontend-angular-dependencies group
Bumps the frontend-angular-dependencies group in /frontend with 1 update: [ngx-echarts](https://github.com/xieziyu/ngx-echarts).


Updates `ngx-echarts` from 17.1.0 to 17.2.0
- [Release notes](https://github.com/xieziyu/ngx-echarts/releases)
- [Changelog](https://github.com/xieziyu/ngx-echarts/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xieziyu/ngx-echarts/compare/v17.1.0...v17.2.0)

---
updated-dependencies:
- dependency-name: ngx-echarts
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 09:34:31 +00:00
softsimon
4364b5fb06 Merge pull request #5096 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.10.0
Bump cypress from 13.9.0 to 13.10.0 in /frontend
2024-05-22 16:33:04 +07:00
softsimon
86bc7b3f37 Pull from transifex 22/5 2024-05-22 16:32:45 +07:00
softsimon
ea0a4d1d67 Liquid gets block reward data from coinbase 2024-05-22 16:16:47 +07:00
softsimon
aa80fa550b More refactoring based on feedback 2024-05-22 15:28:27 +07:00
dependabot[bot]
bdbe833e2a ---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 02:42:52 +00:00
dependabot[bot]
c1f338774a ---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 02:21:26 +00:00
Mononaut
e5ab8ab732 Update default faucet min, revalidate on change 2024-05-21 22:12:33 +00:00
Mononaut
ce204d03c6 Fix local acceleration filter & reindex 2024-05-21 21:47:18 +00:00
Hans ❤️ Crypto
0e37e85af6 Create hans-crypto.txt 2024-05-21 12:35:20 +02:00
natsoni
b0630de3cc Merge branch 'master' into natsoni/statistics-replication 2024-05-21 11:35:26 +02:00
Hans ❤️ Crypto
4b3123b5ae Remove reference to bisq in unfurler
not needed anymore
2024-05-21 09:00:05 +02:00
wiz
2ff2b78d26 Merge pull request #5087 from TheBlueMatt/patch-1
Update empty block explainer to remove nonsense information
2024-05-21 10:47:08 +09:00
Mononaut
35ff2c7225 Minor faucet UI refactor 2024-05-21 00:56:24 +00:00
Mononaut
69786d5b4b Update onbtc preview fallback image 2024-05-20 23:48:53 +00:00
softsimon
3c3ab96164 Fix liquid testnet block test 2024-05-21 00:48:00 +07:00
softsimon
0c49c5313b Scroll down block page in e2e tests 2024-05-21 00:01:40 +07:00
Matt Corallo
c5f6509a4c Update empty block explainer to remove nonsense information
In #5061 it was suggested that the primary cause of empty blocks was due to ASICs taking some time to switch to new work, but this is not a possible reason for empty blocks - pools can mine empty or full blocks no matter how long ASICs do (or do not) take to switch to new work.

At some time T a new block is found, at some time T+1 a pool hears about this new block and desires to switch their miners to the new block. If the pool does a full block validation of the new block prior to sending new work (and updates their mempool), they can switch miners to the new work with transactions immediately. If, then, at time T+2 the ASIC finds a block based on the prior work it doesn't matter whether they were given transactions or not - the block is based on the previous tip and is just a fork.

Instead, the relevant question for empty blocks is whether the pool delays until they can fully validate the block and update their mempool (or in fact even *have* the block), selecting a first block template which contains transactions or not. #5061 tried to explain this away with an incredibly weak reference to bandwidth, but the cost of a merkle branch for a block with 4k transactions is only ~800 bytes of JSON'd hex, which shouldn't even push you into a second IP packet, let alone make for substantial bandwidth.

Its worth noting that it can take many seconds for block to make it from one pool to others, and can further take a second or two to validate a block even on fast hardware (in cases where Bitcoin Core decides to flush to disk due to cache fill), so it makes sense that you'll see empty blocks for some seconds after the previous block. @mononaut posted a chart convincingly demonstrating this - for the first few seconds after a previous block ~all blocks are empty blocks (strong evidence for spy mining or otherwise sending new work prior to updating the local mempool) and we see some dropoff of empty blocks therafter on a relatively expected distribution.

There are likely some empty blocks which are mined further past the previous block due to ASICs holding onto previous work, however the only reason those ASICs *have* previous work that is an empty block is because of spy mining or otherwise not tuning Bitcoin Core on the side of the pool to ensure they can update their mempool fast enough and get blocks fast enough to not need to send empty work at all.
2024-05-20 00:39:13 +00:00
softsimon
8bac2db12c Fix 3rd party licenses link 2024-05-20 00:43:58 +07:00
nymkappa
0605e80d89 Merge branch 'master' into nymkappa/prepaid-update-price 2024-05-19 08:10:23 +02:00
softsimon
abad704fc6 Merge branch 'master' into simon/refactor-block-page 2024-05-19 11:53:54 +07:00
softsimon
d7459ba943 Merge pull request #5084 from mempool/nymkappa/testnet4-tests
[tests] testnet -> testnet4
2024-05-19 11:53:43 +07:00
wiz
ec7da01186 Fix broken TX link from faucet component 2024-05-19 09:59:56 +09:00
wiz
2ce1cc24b9 Merge pull request #5080 from mempool/nymkappa/faucet-polish
[faucet] polish code and UX - Move Twitter login component into FOSS
2024-05-19 08:25:08 +09:00
nymkappa
cb95423178 [tests] testnet -> testnet4 2024-05-18 20:52:45 +02:00
softsimon
a68c8d317c Refactor block transactions 2024-05-19 00:39:33 +07:00
nymkappa
f58c2e6fe5 [faucet] add more error string 2024-05-18 18:56:02 +02:00
nymkappa
203e2904d6 [faucet] block coins request to faucet change address 2024-05-18 16:13:58 +02:00
nymkappa
38971d33a6 [faucet] handle new faucet status codes 2024-05-18 11:20:07 +02:00
nymkappa
83f3d07538 [faucet] only enable on mempool.space instance 2024-05-18 10:54:06 +02:00
nymkappa
9e844ffbbd [faucet] disable pointer when not available, fix unsubscribe() 2024-05-18 10:43:19 +02:00
wiz
0497c750be Merge pull request #5066 from mempool/mononaut/hide-t4-ln-graphs
Remove all lightning elements on unsupported networks
2024-05-18 07:19:35 +09:00
Mononaut
788caf05ce Remove all lightning elements on unsupported networks 2024-05-17 22:02:16 +00:00
softsimon
db34ca6a5f Merge pull request #5061 from BitcoinMechanic/update-empty-block-explainer
update empty block explainer, add contributor info
2024-05-18 01:45:55 +07:00
nymkappa
3bbdf6fb25 [faucet] improve feedback upon success 2024-05-17 20:45:39 +02:00
nymkappa
6b16b2d651 [faucet] polish code and UX - Move Twitter login component into FOSS 2024-05-17 20:32:43 +02:00
wiz
0279fe69d4 Fix footer links for Sign In / My Account 2024-05-17 17:54:06 +09:00
wiz
e1c47fddee Merge pull request #5075 from mempool/mononaut/testnet4-faucet
Faucet
2024-05-17 17:40:24 +09:00
wiz
6b2b3459f7 Merge branch 'master' into update-empty-block-explainer 2024-05-17 15:43:53 +09:00
softsimon
1d91e76ec2 Merge pull request #5033 from mempool/natsoni/add-block-fee-graph
Add block fees vs subsidy bar graph
2024-05-17 11:47:58 +07:00
Mononaut
e8dbd777d0 Faucet arguments as query params 2024-05-16 21:17:39 +00:00
Mononaut
987def9baa Improve faucet mobile layout 2024-05-16 21:10:15 +00:00
Mononaut
d9197e28be Fix faucet 403 handling 2024-05-16 21:09:47 +00:00
Mononaut
72f4d811c1 Add missing websocket subscription to faucet component 2024-05-16 19:16:53 +00:00
Mononaut
f8144bd9a0 Fetch faucet address from services backend 2024-05-16 18:36:33 +00:00
BitcoinMechanic
d6d56688ea Update api-docs.component.html
Tweak empty block explainer
2024-05-16 10:12:09 -07:00
softsimon
97817e770f Merge pull request #5074 from mempool/mononaut/meta-slurper
Preserve site-specific meta title/descriptions/canonical urls
2024-05-16 23:19:03 +07:00
Mononaut
bbf8aed38f Add faucet navbar item on official testnet4 2024-05-16 07:36:13 +00:00
Mononaut
ef5c8ddcdf Add testnet4 faucet 2024-05-16 07:35:55 +00:00
Mononaut
69e1d6150d Preserve site-specific meta title/descriptions 2024-05-15 20:57:29 +00:00
natsoni
86c3ef68e4 Graph: don't refresh data if timespan is 24h or 3d 2024-05-15 17:05:18 +02:00
natsoni
ec54ed6f94 Remove bar graph data update logic and fix mobile navigation 2024-05-15 13:07:32 +02:00
natsoni
c2a3ff4b67 Improve granularity when zoom in block fees bar graph 2024-05-14 20:31:25 +02:00
softsimon
cc9e4f2d43 Merge pull request #5073 from mempool/simon/fix-themeservice-not-loaded
Fix themeService not loaded
2024-05-14 22:26:52 +07:00
softsimon
4c62d54b6b Fix themeService not loaded 2024-05-14 21:47:53 +07:00
softsimon
8bbc27dae5 Merge pull request #5072 from mempool/mononaut/fix-frontend-index-build
Fix index.html generation for custom frontend builds
2024-05-14 15:01:05 +07:00
Mononaut
2ffaeb50df Fix index.html generation for custom frontend builds 2024-05-14 00:15:36 +00:00
wiz
12d212b89f Merge pull request #5071 from mempool/mononaut/fix-customizejs-caching
Fix nginx caching for customize.js resource
2024-05-14 02:12:46 +09:00
Mononaut
1cdb38af0b Fix nginx caching for customize.js resource 2024-05-13 16:57:14 +00:00
softsimon
947f55dda2 Merge pull request #5070 from mempool/dependabot/docker/docker/frontend/node-20.13.1-buster-slim
Bump node from 20.12.0-buster-slim to 20.13.1-buster-slim in /docker/frontend
2024-05-13 23:52:48 +07:00
softsimon
c52bde140f Merge pull request #5069 from mempool/dependabot/docker/docker/backend/node-20.13.1-buster-slim
Bump node from 20.12.0-buster-slim to 20.13.1-buster-slim in /docker/backend
2024-05-13 23:52:34 +07:00
wiz
1663637ed9 ops: Keep non-mainnet networks' maxmempool at default size 2024-05-13 23:20:24 +09:00
wiz
d24217282b ops: Fix wrong port in onbtc unfurler config 2024-05-13 20:58:57 +09:00
softsimon
b6a1547ce4 Merge pull request #5034 from mempool/natsoni/fix-unnecessary-load-more
Prevent address page to load more txs unnecessarily
2024-05-13 17:19:28 +07:00
softsimon
66f431d3d3 Force curly if-statements 2024-05-13 17:16:28 +07:00
softsimon
d02625eb0d Merge branch 'master' into natsoni/fix-unnecessary-load-more 2024-05-13 17:09:50 +07:00
softsimon
a3ac9c31ed Merge pull request #5067 from mempool/hunicus/bird-x
Replace twitter logo with x logo
2024-05-13 16:24:35 +07:00
softsimon
4729a87b39 Slight width reduction 2024-05-13 16:24:17 +07:00
BitcoinMechanic
16c154f39d Update frontend/src/app/docs/api-docs/api-docs.component.html
Co-authored-by: mononaut <83316221+mononaut@users.noreply.github.com>
2024-05-12 23:05:55 -07:00
softsimon
fa98c73067 Merge pull request #5025 from mempool/hunicus/sv-about
Replace bisq with onbtc for about alliances
2024-05-13 10:12:49 +07:00
softsimon
d32de4fa06 Merge pull request #5068 from mempool/mononaut/handle-services-outage
Fix error handling for non-critical services API requests
2024-05-13 10:03:13 +07:00
dependabot[bot]
50ee7c07d1 Bump node in /docker/frontend
Bumps node from 20.12.0-buster-slim to 20.13.1-buster-slim.

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 02:59:07 +00:00
dependabot[bot]
2ac041dc6c Bump node in /docker/backend
Bumps node from 20.12.0-buster-slim to 20.13.1-buster-slim.

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 02:44:50 +00:00
hunicus
4aac31332f Merge branch 'master' into hunicus/sv-about 2024-05-13 06:23:22 +09:00
hunicus
9a6ac69983 Update onbtc image and url 2024-05-12 17:21:58 -04:00
Mononaut
c93aa4d82c add catchError fallback for non-critical services API requests 2024-05-12 21:16:00 +00:00
hunicus
16fe1f3cec Merge branch 'master' into hunicus/bird-x 2024-05-13 05:25:54 +09:00
natsoni
42d591bf4c Block subsidy graph: add lower timeframes 2024-05-12 21:52:53 +02:00
wiz
2133356047 Merge pull request #5065 from mempool/mononaut/balance-widget-periods
Update balance widget 7d/30d intervals
2024-05-13 01:31:48 +09:00
Mononaut
d7b0923000 Update balance widget 7d/30d intervals 2024-05-12 16:27:57 +00:00
wiz
2fa33b749c Update el salvador twitter handle again 2024-05-13 01:23:32 +09:00
wiz
90cc75479e Merge pull request #5064 from mempool/mononaut/update-tos-onbtc
Update terms of service
2024-05-13 01:07:47 +09:00
wiz
e2eadd0433 Merge pull request #5063 from mempool/mononaut/fix-x-widget-handle
Fix x widget
2024-05-13 01:05:17 +09:00
Mononaut
1bd045582d Update terms of service 2024-05-12 16:05:02 +00:00
Mononaut
c6e8885c94 fix x widget handle handling 2024-05-12 16:00:44 +00:00
wiz
5531d04f10 Change twitter timeline acct bitcoinofficesv to nayibbukele 2024-05-13 00:51:32 +09:00
wiz
85ff8521f7 ops: Update bitcoin.crontab for testnet4 2024-05-13 00:51:28 +09:00
wiz
3794ddfd3a ops: Update bitcoin.conf for testnet4 2024-05-13 00:51:25 +09:00
Mononaut
568084e143 Configurable threshold for esplora tip check 2024-05-12 00:35:25 +00:00
BitcoinMechanic
64d979d5f9 update empty block explainer, add contributor info 2024-05-11 11:39:46 -07:00
natsoni
0ef76f3e64 Block subsidy graph back to timespan selection mode 2024-05-11 14:36:17 +02:00
softsimon
b8725ab13a Merge pull request #5054 from mempool/mononaut/fix-statistics-graphs
Fix statistics graphs
2024-05-10 14:12:34 +07:00
wiz
6bf47f69eb Merge pull request #5056 from mempool/mononaut/unfurl-descriptions
temporarily hardcode unfurl titles & descriptions
2024-05-10 06:13:25 +09:00
wiz
0192c2bffa ops: Modify build script to build unfurlers last 2024-05-10 05:41:46 +09:00
Mononaut
91aa815d88 temporarily hardcode unfurl titles & descriptions 2024-05-09 20:01:13 +00:00
wiz
53a493c233 Merge branch 'master' into natsoni/add-block-fee-graph 2024-05-10 04:59:17 +09:00
wiz
dcdb9361e8 Merge pull request #5055 from mempool/mononaut/custom-build-html
Update custom sv html
2024-05-10 04:22:15 +09:00
Mononaut
89531d1703 Update custom sv html 2024-05-09 19:18:17 +00:00
wiz
8311e842bb ops: Enable testnet4 in prod frontend configs 2024-05-10 04:02:35 +09:00
wiz
a492223369 Add warning that testnet4 may be reset at anytime 2024-05-10 03:57:25 +09:00
Mononaut
cd9e336165 Also fix stats on custom dashboard & tv view 2024-05-09 18:05:01 +00:00
Mononaut
5b2ecac1f6 Fix statistics graphs 2024-05-09 18:00:09 +00:00
wiz
d2f7864266 ops: Don't serve stale cache data for non-mainnet (2/2) 2024-05-10 01:14:51 +09:00
wiz
c9432aa0d1 Merge pull request #5053 from mempool/mononaut/unfurler-fixes-2
Fix unfurler resources build
2024-05-10 00:56:56 +09:00
Mononaut
eeb2bae9f3 Fix unfurler resources build 2024-05-09 15:55:03 +00:00
wiz
659ef0b6b9 ops: Don't serve stale cache data for non-mainnet 2024-05-10 00:44:53 +09:00
wiz
9b83ac29b7 Merge pull request #5050 from mempool/mononaut/t4-unfurls
Enable testnet4 unfurls
2024-05-09 23:42:39 +09:00
wiz
722b81623b Merge pull request #5051 from mempool/mononaut/fix-onbtc-fallback-img
Fix onbtc unfurler fallback image
2024-05-09 23:42:26 +09:00
wiz
648dd5adb2 Merge pull request #5052 from mempool/mononaut/testnet3-banner
Update testnet3 banner
2024-05-09 23:42:11 +09:00
Mononaut
75e7bb3255 Update testnet3 banner 2024-05-08 20:46:55 +00:00
Mononaut
10fc4d27ad Fix onbtc unfurler fallback image 2024-05-08 19:59:50 +00:00
Mononaut
ace422c0d5 Enable testnet4 unfurls 2024-05-08 19:58:57 +00:00
softsimon
b6682eed2a Merge pull request #5048 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.9.0
Bump cypress from 13.8.0 to 13.9.0 in /frontend
2024-05-09 00:09:55 +07:00
softsimon
8080b8b56a Remove Bukele theme option 2024-05-08 23:58:51 +07:00
softsimon
0e8d04e464 Merge pull request #5049 from mempool/natsoni/fix-navbar
Fix transparent navbar
2024-05-08 23:30:33 +07:00
natsoni
fc76146085 Fix transparent navbar 2024-05-08 18:25:29 +02:00
softsimon
4142413002 Merge pull request #5028 from mempool/mononaut/x-widget
Add x widget
2024-05-08 22:57:21 +07:00
softsimon
49e4855982 Merge branch 'master' into mononaut/x-widget 2024-05-08 22:56:03 +07:00
dependabot[bot]
65cad2025a Bump cypress from 13.8.0 to 13.9.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.8.0 to 13.9.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/commits)

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 18:34:47 +00:00
Mononaut
f83404421a Add x widget 2024-05-01 23:59:02 +00:00
wiz
cd8abe5c06 ops: Tweak http port for onbtc unfurler instance 2024-05-02 00:53:14 +09:00
softsimon
471a2397c0 Add customize.js to .gitignore 2024-05-01 22:51:58 +07:00
hunicus
bf541f0898 Replace bisq with onbtc for about alliances 2024-04-29 22:39:10 -04:00
Mononaut
a582a3b0ed Polish address balance graph, add period toggle 2024-04-29 21:03:09 +00:00
softsimon
20df70c449 Fix missing return on invalid params 2024-04-30 02:02:53 +07:00
wiz
af13b6c9f8 Merge pull request #5024 from mempool/mononaut/onbtc-siteid
Add onbtc site id
2024-04-30 01:33:36 +09:00
Mononaut
06ee82af0b Add onbtc site id 2024-04-29 14:36:45 +00:00
natsoni
57b466d80f Update block display default mode to fees 2024-04-29 11:54:28 +02:00
wiz
dbd4d152ce Merge pull request #5020 from mempool/mononaut/custom-unfurls
custom unfurls
2024-04-29 17:37:11 +09:00
wiz
95f47409d8 Add onbtc frontend config, add onbtc backend to build script 2024-04-29 17:29:45 +09:00
wiz
ed64b72676 ops: Add onbtc frontend config 2024-04-29 17:20:35 +09:00
wiz
f9948055e4 ops: Update build/unfurler stuff for onbtc 2024-04-29 17:12:51 +09:00
dependabot[bot]
6ad709f93a Bump ws from 8.16.0 to 8.17.0 in /backend
Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 02:16:33 +00:00
dependabot[bot]
f9498cf5a2 Bump nginx from 1.25.4-alpine to 1.26.0-alpine in /docker/frontend
Bumps nginx from 1.25.4-alpine to 1.26.0-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 02:06:21 +00:00
Mononaut
f5333c529c Update unfurler routing, add fallback img 2024-04-28 16:18:35 +00:00
Mononaut
6b2cc23cd3 Add custom branding to unfurler previews 2024-04-28 16:18:16 +00:00
Mononaut
34329d04f0 favicons 2024-04-28 16:02:42 +00:00
Mononaut
701c283e97 Fix svg 2024-04-28 16:02:41 +00:00
Mononaut
f9fd589af2 Logo overrides, custom index.html 2024-04-28 16:02:41 +00:00
softsimon
0d9602693b Correcting duplicate string 'Date' 2024-04-28 09:46:55 +07:00
softsimon
27be568eb5 Fix duplicate "sats" string 2024-04-28 09:43:35 +07:00
softsimon
8f06aea736 Extracting new i18n strings 2024-04-28 09:39:04 +07:00
wiz
65191538e2 Merge pull request #5016 from mempool/mononaut/customization
Customized frontend builds
2024-04-28 04:11:11 +09:00
nymkappa
8b1acbe13b [account] update profile image md5 2024-04-27 14:49:06 +02:00
Mononaut
d49485c363 Update custom address widgets on new block transactions 2024-04-26 23:59:27 +00:00
Mononaut
1de028840e Custom address widget title links 2024-04-26 20:06:34 +00:00
Mononaut
3f9fb68c5f Fix mobile custom enterprise logo 2024-04-26 20:01:00 +00:00
Mononaut
33ef15511f Check in missing address transactions widget component 2024-04-26 19:41:11 +00:00
Mononaut
139c384e97 Add address transactions widget 2024-04-26 19:17:28 +00:00
Mononaut
ccb27dbdb9 Address widget timespans 2024-04-26 19:17:27 +00:00
Mononaut
6eb21ffd24 check in svg 2024-04-26 19:17:27 +00:00
Mononaut
b9b3c8f78a Custom branding 2024-04-26 19:17:27 +00:00
Mononaut
4f4215577a Add custom dashboard component 2024-04-26 19:17:27 +00:00
Mononaut
ac0f56325b Add customizable enterprise build 2024-04-26 19:17:27 +00:00
softsimon
fd8df2a035 Merge pull request #5015 from mempool/natsoni/add-toggle-sats
Add an option to display values as sats
2024-04-26 12:58:52 +07:00
softsimon
b4decb8f91 Merge pull request #5014 from mempool/natsoni/small-fixes-liquid-frontend
Minor changes on Liquid frontend
2024-04-26 12:07:07 +07:00
softsimon
836c1e002e Merge pull request #4981 from mempool/natsoni/fix-tx-details-loading
Fix miner data loading in transaction details
2024-04-26 12:06:03 +07:00
softsimon
701701e13c Merge pull request #5013 from mempool/natsoni/fix-liquid-footer
Remove Mining dashboard from Liquid footer
2024-04-26 11:54:36 +07:00
wiz
6accf8420f Merge branch 'master' into nymkappa/prepaid-update-price 2024-04-25 02:24:42 +09:00
natsoni
ea5c3212d6 Add an option to display values as sats 2024-04-24 16:44:56 +02:00
natsoni
65705a8a1d Fix Liquid UTXOs table color 2024-04-24 14:19:14 +02:00
natsoni
8310ad9f24 Fix gauge clickable pointer 2024-04-24 14:17:50 +02:00
natsoni
1a5c2c4d3a Add a 5% tolerance for declaring a "unpeg event" 2024-04-24 14:17:40 +02:00
natsoni
cd689afaaa Add config check to show mining dashboard in footer 2024-04-24 11:00:51 +02:00
softsimon
00e0eea60e Merge pull request #5012 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.9.7
Bump mysql2 from 3.9.4 to 3.9.7 in /backend
2024-04-24 10:38:42 +07:00
softsimon
d89ba767f1 Merge pull request #5011 from mempool/mononaut/rbf-mobile-autoscroll-bug
Fix rbf autoscroll bug on mobile
2024-04-24 10:34:34 +07:00
dependabot[bot]
c4acc95b06 Bump mysql2 from 3.9.4 to 3.9.7 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.9.4 to 3.9.7.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.9.4...v3.9.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-23 23:11:18 +00:00
nymkappa
ed9f792d21 fix typo 2024-04-23 17:54:04 +02:00
Mononaut
31c008afa5 Fix rbf autoscroll bug on mobile 2024-04-22 21:52:48 +00:00
orangesurf
6e2c0bac43 Update accelerate-checkout.component.ts
Analysis suggests 1.5 would be a good starting point

https://gist.github.com/orangesurf/5f69da2ffbdd1b737be53789e1783b03
2024-04-22 20:18:40 +02:00
softsimon
a66920c51e Merge pull request #5004 from mempool/natsoni/toggle-block-fees
Add block display mode toggle
2024-04-22 20:30:26 +07:00
softsimon
1c9e2b546b Replacing setTimeouts with rxjs 2024-04-22 14:42:48 +07:00
nymkappa
9363004252 [accelerator] change default bid prepaid 2024-04-22 08:08:03 +02:00
softsimon
2297ff72f5 Merge pull request #5006 from mempool/knorrium/increase_language_selector_width
Knorrium/increase language selector width
2024-04-22 10:24:38 +07:00
Felipe Knorr Kuhn
fbaa39a300 Change only the selector width 2024-04-21 19:34:48 -07:00
Felipe Knorr Kuhn
bd2c15c6bb Increase the language selector width to accomodate all languages 2024-04-21 18:48:44 -07:00
natsoni
a4d8f2db58 Add block display mode toggle button 2024-04-21 14:54:50 +02:00
Mononaut
2a43255802 testmempool accept more validation & switch to JSON array format 2024-04-21 10:05:58 +07:00
Mononaut
f3232b2d5c Add Test Transactions page 2024-04-21 10:05:58 +07:00
Mononaut
c7cb7d1ac4 Add testmempoolaccept API endpoint 2024-04-21 10:05:57 +07:00
softsimon
bd5a23ff0d Merge pull request #4997 from mempool/natsoni/fix-dropdown-focus
Fix dropdown menu navigation with arrow keys
2024-04-20 08:35:17 +07:00
natsoni
dd0c1eb74d Fix dropdown menu arrow navigation 2024-04-20 00:57:59 +02:00
softsimon
9950ef16ce Merge pull request #4995 from mempool/natsoni/fix-pagination-color
Fix table pagination colors on custom themes
2024-04-20 01:25:56 +07:00
softsimon
fed2c034ee Merge pull request #4996 from mempool/nymkappa/update-doc
[doc] update accel doc
2024-04-20 00:47:03 +07:00
nymkappa
4944e3c257 [doc] update accel doc 2024-04-19 18:45:59 +02:00
natsoni
b865fa33f6 Fix table pagination colors 2024-04-19 17:08:19 +02:00
softsimon
bb44ed15da Merge pull request #4991 from mempool/nymkappa/unique-node-addr
[lightning] unique sockets for ln nodes
2024-04-19 18:02:42 +07:00
softsimon
93919421ee Merge pull request #4984 from mempool/natsoni/more-css
More css fixes
2024-04-19 16:24:25 +07:00
softsimon
8d1831129b Merge pull request #4988 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.8.0
Bump cypress from 13.7.0 to 13.8.0 in /frontend
2024-04-19 16:23:30 +07:00
softsimon
5e6e2c037e Merge pull request #4979 from mempool/natsoni/improve-tooltip-positioning
Fix cropped transaction tooltip in block overview on mobile
2024-04-19 16:23:10 +07:00
nymkappa
ddfceddc57 [lightning] unique sockets for ln nodes 2024-04-19 10:56:15 +02:00
softsimon
5b19ffd524 Pull from transifex 2024-04-19 14:53:32 +07:00
wiz
7422001ed5 Merge pull request #4990 from mempool/nymkappa/disable-topology-prod
[prod] disable LN TOPOLOGY_FOLDER by default
2024-04-19 16:27:16 +09:00
nymkappa
3b0ef48cdf [prod] disable LN TOPOLOGY_FOLDER by default 2024-04-19 09:06:10 +02:00
wiz
d13875e3e7 Merge pull request #4989 from mempool/nymkappa/update-doc
[doc] update
2024-04-19 16:06:03 +09:00
nymkappa
6ebc7fab47 [doc] update to match accel history endpoint actual response 2024-04-19 08:55:21 +02:00
dependabot[bot]
9d931c1d13 Bump cypress from 13.7.0 to 13.8.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.7.0 to 13.8.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.7.0...v13.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-19 02:15:31 +00:00
softsimon
f86b6ac3de Merge pull request #4985 from mempool/mononaut/fix-audit-bug
Fix audit blockIndex increment bug
2024-04-19 00:13:48 +07:00
natsoni
ce1124284e Adapt more css for color theme 2024-04-18 16:18:38 +02:00
Mononaut
233f19d23d Fix audit blockIndex increment bug 2024-04-18 13:27:33 +00:00
natsoni
89ef5fe33d Fix tooltip text overflow in some languages 2024-04-18 11:09:28 +02:00
softsimon
7cd4345264 Merge pull request #4956 from henrialb/typo
Fix typo in docs
2024-04-18 11:59:57 +07:00
natsoni
bbaa5dafb9 Separate mining and audit subscriptions 2024-04-17 16:41:30 +02:00
wiz
cee1b39640 Merge pull request #4980 from mempool/mononaut/fix-acc-button-conditions 2024-04-17 04:06:27 +09:00
Mononaut
e388410e34 Re-add accelerate button on deep mempool txs 2024-04-16 18:52:13 +00:00
natsoni
c2d649d3f4 Improve tooltip position in block overview on mobile 2024-04-16 16:02:39 +02:00
wiz
d9dec44c8f Merge pull request #4977 from mempool/nymkappa/fix-copy
[accelerator] fix pizza accel copy/eta
2024-04-16 21:14:56 +09:00
wiz
c94a3a98eb Change "Settlement" to "Confirmation" 2024-04-16 21:14:45 +09:00
nymkappa
ec9b8359df [accelerator] fix pizza accel copy/eta x2 2024-04-16 21:12:25 +09:00
nymkappa
3db89e3dee [accelerator] fix pizza accel copy/eta 2024-04-16 21:09:30 +09:00
wiz
7f8dc74146 Merge pull request #4975 from mempool/nymkappa/mini-branch
[accelerator] polish pizza accel
2024-04-16 17:11:55 +09:00
wiz
83cea33727 Merge pull request #4969 from mempool/nymkappa/missing-accelerate-button-mobile
[accelerator] re-add missing accelerator button on mobile, set locati…
2024-04-16 17:02:25 +09:00
nymkappa
ada18a0413 [accelerator] fix redirects 2024-04-16 17:02:10 +09:00
nymkappa
96d85dcacd [accelerator] polish pizza accel x2 2024-04-16 16:56:37 +09:00
nymkappa
64ba033602 [accelerator] polish pizza accel 2024-04-16 16:01:52 +09:00
nymkappa
26807e80db Merge branch 'nymkappa/accel-summary-default' into nymkappa/mini-branch 2024-04-16 14:43:01 +09:00
nymkappa
e4f00285b3 Merge branch 'nymkappa/polish-accel' into nymkappa/mini-branch 2024-04-16 14:42:53 +09:00
nymkappa
d443e51d30 [accelerator] if eligible, show pizza accel summary by default 2024-04-16 11:45:04 +09:00
softsimon
b4352f5f25 Merge pull request #4972 from mempool/dependabot/github_actions/dtolnay/rust-toolchain-bb45937a053e097f8591208d8e74c90db1873d07
Bump dtolnay/rust-toolchain from dc6353516c68da0f06325f42ad880f76a5e77ec9 to bb45937a053e097f8591208d8e74c90db1873d07
2024-04-15 18:10:12 +07:00
softsimon
2c1b4a2547 Merge pull request #4955 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.9.4
Bump mysql2 from 3.9.1 to 3.9.4 in /backend
2024-04-15 18:09:56 +07:00
softsimon
b54685bed7 Merge pull request #4973 from mempool/natsoni/fix-dropdown-menu-color
Adapt dropdown menu color to theme
2024-04-15 18:02:54 +07:00
natsoni
9032d5ac11 Adapt dropdown menu color to theme 2024-04-15 12:35:15 +02:00
dependabot[bot]
942747a0be Bump dtolnay/rust-toolchain
Bumps [dtolnay/rust-toolchain](https://github.com/dtolnay/rust-toolchain) from dc6353516c68da0f06325f42ad880f76a5e77ec9 to bb45937a053e097f8591208d8e74c90db1873d07.
- [Release notes](https://github.com/dtolnay/rust-toolchain/releases)
- [Commits](dc6353516c...bb45937a05)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-15 03:00:06 +00:00
nymkappa
fdd6463ac0 [ui] polish pizza accelerator 2024-04-14 16:29:56 +09:00
nymkappa
e9afc4ec0f remove duplicated condition 2024-04-14 13:55:49 +09:00
nymkappa
dff811f615 [accelerator] re-add missing accelerator button on mobile, set location.hash when click on accelerate button 2024-04-14 13:54:25 +09:00
wiz
d1e382ddf7 Merge pull request #4967 from mempool/mononaut/fix-acc-fee-rate-again
Fix accelerated fee rate again
2024-04-14 13:20:06 +09:00
wiz
ccd460cf70 Merge pull request #4968 from mempool/mononaut/54-year-pizza
Fix first seen loading time in tx tracker
2024-04-14 13:18:56 +09:00
Mononaut
7b1ba912be Fix first seen loading time in tx tracker 2024-04-14 03:49:26 +00:00
Mononaut
f3864c9100 Fix accelerated fee rate again 2024-04-14 03:21:24 +00:00
wiz
1082889b64 Merge pull request #4966 from mempool/nymkappa/fix-fee-paid
[accelerator] fix fee paid calculation
2024-04-14 00:20:42 +09:00
nymkappa
36839fdcb9 [accelerator] fix fee paid calculation 2024-04-14 00:19:14 +09:00
wiz
ebe54d47d8 Merge pull request #4965 from mempool/nymkappa/remove-prepaid-tx
[accelerator] remove simple mode from tx page
2024-04-14 00:15:02 +09:00
nymkappa
7219823847 [accelerator] remove simple mode from tx page 2024-04-14 00:10:15 +09:00
wiz
336fc7a64a Merge pull request #4964 from mempool/mononaut/bitcoin-tracker
Add Bitcoin logo to tracker header
2024-04-13 23:20:46 +09:00
Mononaut
84f62f8025 Add Bitcoin logo to tracker page 2024-04-13 14:18:01 +00:00
wiz
6bd6dfec49 Merge pull request #4963 from mempool/nymkappa/accel-checkout
[accelerator] remove test code
2024-04-13 23:13:29 +09:00
nymkappa
a6e27f1312 Merge branch 'master' into nymkappa/accel-checkout 2024-04-13 23:13:02 +09:00
nymkappa
c913df837a [accelerator] remove test code 2024-04-13 23:12:05 +09:00
wiz
42bd9811e3 Merge pull request #4962 from mempool/nymkappa/accel-checkout
[accelerator] polish prepaid accel UI
2024-04-13 23:09:03 +09:00
wiz
7fbf13fd9d Merge pull request #4961 from mempool/mononaut/pizza-layout
Change tx tracker layout
2024-04-13 23:08:10 +09:00
nymkappa
fb086b5ad5 [accelerator] polish UI prepaid accel 2024-04-13 23:07:19 +09:00
Mononaut
d4f51979d4 Change tx tracker layout 2024-04-13 13:56:29 +00:00
wiz
aeb54f59f1 Merge pull request #4960 from mempool/mononaut/change-toppings
change pizza tracker toppings
2024-04-13 22:17:10 +09:00
Mononaut
5ca3b24527 Fix accelerate arrow, add accelerating status 2024-04-13 13:14:26 +00:00
Mononaut
88df2878cb Add logo to tx tracker page, fix bugs 2024-04-13 12:26:30 +00:00
nymkappa
f601bbc499 Merge branch 'master' into nymkappa/accel-checkout 2024-04-13 21:13:11 +09:00
nymkappa
24e9ae6440 [accelerator] re-integrate square payment WIP 2024-04-13 20:53:19 +09:00
wiz
8c3f11622c Merge pull request #4959 from mempool/mononaut/pizza
WIP (work-in-pizza)
2024-04-13 19:02:22 +09:00
Mononaut
da25ed431f hijack /tx/ page for tracker users 2024-04-13 09:24:52 +00:00
Mononaut
ecc234a96a Tracker bottom panel with status icon 2024-04-13 09:09:14 +00:00
Mononaut
29851537eb Prototype accelerate checkout properties 2024-04-13 09:09:13 +00:00
nymkappa
5f2d7b32ae [accelerator] concept accelerator a/b ctas 2024-04-13 09:09:13 +00:00
Mononaut
de00d49d7b Basic tracker page structure 2024-04-13 09:09:09 +00:00
wiz
bafa0f50fc Merge pull request #4958 from mempool/wiz/20240413-add-new-lightning-nodes
Add new VA1/FRA lightning nodes
2024-04-13 17:56:13 +09:00
wiz
80c75f9aeb Add new FRA lightning nodes 2024-04-13 17:49:29 +09:00
wiz
407eba194d Add new VA1 lightning nodes 2024-04-13 17:37:54 +09:00
Mononaut
2eac3e6555 Add pizza tracker component 2024-04-13 08:05:31 +00:00
Mononaut
61a308cbc6 Add simplified tracker blockchain component 2024-04-13 08:04:58 +00:00
nymkappa
49b9a6f53d [accelerator] concept accelerator a/b ctas 2024-04-13 16:11:49 +09:00
wiz
db1cc0d0e1 Merge pull request #4953 from mempool/nymkappa/use-bid-boost-accel-list
[accelerator] use new bidBoost field
2024-04-13 14:59:33 +09:00
Henrique Albuquerque
f89bd5b972 Add contributor statement 2024-04-12 18:05:18 +01:00
Henrique Albuquerque
d94a8cb58c Fix typo 2024-04-12 17:59:31 +01:00
dependabot[bot]
faf79e85fd Bump mysql2 from 3.9.1 to 3.9.4 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.9.1 to 3.9.4.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.9.1...v3.9.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-12 16:14:46 +00:00
softsimon
0d72e88c6a Updated from transifex 2024-04-12 16:18:39 +09:00
Mononaut
786ec7fff1 Mobile transaction tracker page 2024-04-12 07:16:01 +00:00
softsimon
8df497e53d Merge pull request #4954 from mempool/simon/convert-paid-currency-api
Convert v3 Currency api into v1
2024-04-12 16:07:50 +09:00
softsimon
1dd7a6ebac Convert v3 api into v1 2024-04-12 15:59:33 +09:00
nymkappa
2c12e9f64b [accelerator] use new bidBoost field 2024-04-12 15:12:25 +09:00
softsimon
9a77135d30 Merge pull request #4952 from mempool/simon/paid-currency-api
Paid currency api support
2024-04-12 14:42:56 +09:00
softsimon
f3b3b9b3f0 Adding new config to sample 2024-04-12 14:15:50 +09:00
softsimon
55c4d4d03d Paid currency api support 2024-04-12 14:10:47 +09:00
softsimon
061d341d8b Merge pull request #4949 from mempool/mononaut/delta-sequence
Add sequence number to mempool block updates
2024-04-12 00:30:30 +09:00
softsimon
1c29b8b260 Merge pull request #4950 from mempool/natsoni/fix-timestamp-datetime
Update prices table  'time' field to datetime
2024-04-11 21:07:26 +09:00
natsoni
79bfe9c866 Change time field to datetime 2024-04-11 17:56:34 +09:00
Mononaut
3e6d38656d Add sequence number to mempool block updates 2024-04-11 08:11:45 +00:00
softsimon
5697486cea Merge pull request #4947 from mempool/mononaut/hide-accelerate-panel-already
Hide accelerate panel if not needed
2024-04-11 15:54:55 +09:00
softsimon
df0da605e4 Merge pull request #4948 from mempool/mononaut/54-years
it hasn't been 54 years since this transaction was submitted
2024-04-11 15:54:39 +09:00
Mononaut
fa9aaf0423 Keep firstSeen loader, retry on fail 2024-04-11 06:03:14 +00:00
Mononaut
89de288fec it hasn't been 54 years since this transaction was submitted 2024-04-11 04:02:25 +00:00
Mononaut
940003552b Hide acceleration panel if tx is already accelerated 2024-04-10 11:14:13 +00:00
Mononaut
0660296b51 Disable accelerate button on successful submission 2024-04-10 11:14:13 +00:00
softsimon
affeb0a169 Merge pull request #4943 from mempool/nymkappa/fix-missing-qr-code
[accelerator] fix missing qr code
2024-04-10 19:15:04 +09:00
softsimon
2504653426 Merge pull request #4207 from mempool/mononaut/sipper
Proof-of-concept "sipper" lightweight pages for web crawlers
2024-04-10 18:29:42 +09:00
softsimon
b6a9ad67d3 Merge pull request #4946 from mempool/mononaut/acc-dash-vbytes
Switch success rate to total vsize
2024-04-10 18:12:48 +09:00
Mononaut
3d4741eac2 Fix acceleration skeleton loader titles 2024-04-10 09:03:31 +00:00
softsimon
5611f57e9e Merge pull request #4945 from mempool/mononaut/acc-dash-times
Add timespan toggle to acceleration dashboard
2024-04-10 18:00:25 +09:00
softsimon
44027f5bc0 Pull from transifex 2024-04-10 17:02:35 +09:00
Mononaut
41b25a78e9 adjust accelerator dashboard mobile fields 2024-04-10 07:22:02 +00:00
Mononaut
e263f94765 Switch success rate for total vsize on acceleration dashboard 2024-04-10 06:52:54 +00:00
Mononaut
80c7754e48 Add timespan toggle to acceleration dashboard 2024-04-10 06:27:48 +00:00
softsimon
165340324c Merge pull request #4944 from mempool/natsoni/more-css-fix
More css fixes
2024-04-10 15:15:47 +09:00
softsimon
735dddd604 Merge pull request #4941 from mempool/natsoni/polish-footer
Polish global footer
2024-04-10 15:03:13 +09:00
natsoni
3e3bd32705 Convert more css to variables 2024-04-10 14:41:19 +09:00
softsimon
f83025a9ff Merge pull request #4939 from mempool/hunicus/natsoni-formatting
Make room for natsoni as project member
2024-04-10 14:40:10 +09:00
softsimon
ba6a7b58aa Merge pull request #4942 from mempool/natsoni/fix-header-color-theme
Fix header color theme
2024-04-10 14:37:56 +09:00
nymkappa
fb8808ea59 [accelerator] fix redirect url, fix UX 2024-04-10 14:33:33 +09:00
nymkappa
73f241e9c3 Revert "Disable accelerate button after submission"
This reverts commit e55b1740db.
2024-04-10 13:53:21 +09:00
softsimon
317b1b6ac5 Merge pull request #4909 from mempool/nymkappa/unix-socket
[server] express server also listens on unix socket
2024-04-10 13:41:14 +09:00
natsoni
cabe629f17 Add icon color variable to color themes 2024-04-10 12:26:00 +09:00
natsoni
9f6521b987 Update navbar background color 2024-04-10 12:25:30 +09:00
hunicus
6daffe5b13 Make room for natsoni as project member 2024-04-10 11:39:25 +09:00
nymkappa
4c7f93d1ef fix tests 2024-04-10 09:46:05 +09:00
natsoni
f81bbb93a5 Polish global footer 2024-04-09 20:55:14 +09:00
wiz
b0058e94ce Merge pull request #4938 from mempool/nymkappa/accel-graph
[accelerator] show 1w accel graph by default
2024-04-09 20:17:51 +09:00
nymkappa
de069f704a [accelerator] show 1w accel graph by default 2024-04-09 18:10:53 +09:00
wiz
bcb8493cd0 Merge pull request #4937 from mempool/mononaut/disable-acc-button
disable accelerate button after submission
2024-04-09 18:07:30 +09:00
Mononaut
e55b1740db Disable accelerate button after submission 2024-04-09 08:31:24 +00:00
softsimon
df673b2a4e Pull from transifex 2024-04-09 16:58:02 +09:00
wiz
a4753769d2 Merge pull request #4936 from mempool/natsoni/add-wiz-theme
Add wiz theme
2024-04-09 16:50:31 +09:00
wiz
be75a87e88 Merge pull request #4935 from mempool/mononaut/acc-rate-tooltips
Fix accelerated fee rate in mined block tooltips
2024-04-09 16:50:21 +09:00
nymkappa
0ac3ae1cb1 Update docker/backend/start.sh
Co-authored-by: softsimon <softsimon@users.noreply.github.com>
2024-04-09 16:50:04 +09:00
wiz
a5e1e95534 Fix capitalization of "wiz" theme 2024-04-09 16:49:41 +09:00
natsoni
7f6ab0b854 Add wiz theme 2024-04-09 16:43:02 +09:00
Mononaut
8420ecd380 Fix accelerated fee rate in mined block tooltips 2024-04-09 07:22:24 +00:00
softsimon
192fd09a00 Merge pull request #4933 from mempool/natsoni/fix-address-component-subscriptions
Fix subscription management in address component
2024-04-09 16:18:57 +09:00
softsimon
d9a59c6d1e Accelerator i18n fixes 2024-04-09 16:15:26 +09:00
softsimon
69c3c3162c Theme selector i18n fix 2024-04-09 16:08:58 +09:00
softsimon
61ba832dfd Merge pull request #4932 from mempool/natsoni/remove-console-log
Remove console log
2024-04-09 15:17:34 +09:00
natsoni
f332bba468 Remove console log 2024-04-09 15:14:50 +09:00
nymkappa
ba8cca6ba5 Merge branch 'master' into nymkappa/unix-socket 2024-04-09 15:11:52 +09:00
nymkappa
7a098952c8 [server] disable unix socket listening by default 2024-04-09 15:11:40 +09:00
softsimon
347b74a55d Theme elector width fix 2024-04-09 14:51:54 +09:00
wiz
d5b0adeeed Merge pull request #4931 from mempool/nymkappa/additional-error-message
add additional error message
2024-04-09 14:50:05 +09:00
softsimon
d8c4d36d4b Rename themes 2024-04-09 14:49:24 +09:00
nymkappa
aac32c5bff add additional error message 2024-04-09 14:33:18 +09:00
softsimon
ff86e55622 Merge pull request #4916 from mempool/natsoni/high-contrast-theme
High contrast theme (duplicate)
2024-04-09 14:33:09 +09:00
wiz
894c4cb139 Merge pull request #4930 from mempool/mononaut/filter-acc
Filter accelerations for matching pool
2024-04-09 14:32:27 +09:00
softsimon
2792016383 Fix relative imports 2024-04-09 14:24:34 +09:00
wiz
46215871aa Merge pull request #4928 from mempool/hunicus/bid-boost-link
Make bid boost widget link clickable on mobile
2024-04-09 14:24:28 +09:00
natsoni
cfc06be975 Fix subscription management in address component 2024-04-09 14:21:08 +09:00
natsoni
527589ac04 Merge branch 'master' into natsoni/high-contrast-theme 2024-04-09 12:13:11 +09:00
Mononaut
43845cda5c Filter accelerations for matching pool 2024-04-09 00:08:50 +00:00
wiz
b74b8a8a5a Merge pull request #4929 from mempool/mononaut/fix-acc-rate-labels
Fix accelerated rate labels
2024-04-08 23:13:20 +09:00
Mononaut
226c6d8432 More acceleration labelling fixes 2024-04-08 14:08:11 +00:00
Mononaut
9f79258dec Fix accelerated/effective rate labelling 2024-04-08 13:45:05 +00:00
Mononaut
13bcc99095 Fix block summary data fields 2024-04-08 13:44:08 +00:00
hunicus
fef9b93a05 Merge branch 'master' into hunicus/bid-boost-link 2024-04-08 22:17:10 +09:00
hunicus
ccac3437cf Make bid boost widget link clickable on mobile 2024-04-08 22:09:52 +09:00
wiz
e849a31668 Merge pull request #4926 from mempool/nymkappa/duplicated-accel
[accelerator] fix possible duplicated accel request call
2024-04-08 21:57:17 +09:00
nymkappa
88de5412f8 [accelerator] reset cashapp upon price update 2024-04-08 21:55:47 +09:00
wiz
d13c48d31d Merge pull request #4925 from mempool/simon/fix-first-seen-skeleton
Fix first seen skeleton loader
2024-04-08 21:55:31 +09:00
softsimon
4c807866a3 Fix first seen skeleton loader 2024-04-08 21:44:29 +09:00
nymkappa
d7a4a95c05 [accelerator] avoid duplicated accel request 2024-04-08 21:43:06 +09:00
wiz
b35422ff9f Merge pull request #4922 from mempool/mononaut/actually-fix-sticky-button
Actually fix sticky button
2024-04-08 21:32:28 +09:00
wiz
b6be5d71bb Merge pull request #4923 from mempool/mononaut/wider-sticky-button
Slightly wider sticky button
2024-04-08 21:32:12 +09:00
wiz
4c5eddcf6d Merge pull request #4924 from mempool/mononaut/fix-acc-fee-rate
Fix missing accelerated fee rate again
2024-04-08 21:31:27 +09:00
softsimon
51f0b75a64 Merge pull request #4917 from mempool/natsoni/fix-liquid-dropdown-position
Fix dropdown menu position in Liquid
2024-04-08 21:03:39 +09:00
softsimon
fe4648cd9e Update frontend/src/app/components/liquid-master-page/liquid-master-page.component.scss 2024-04-08 21:03:25 +09:00
softsimon
03867ada49 Merge pull request #4887 from mempool/mononaut/local-acceleration-data
Local acceleration data
2024-04-08 20:43:46 +09:00
Mononaut
7959188c06 Fix missing accelerated fee rate again 2024-04-08 11:36:22 +00:00
Mononaut
11eaa0ca50 Fix NaN boost 2024-04-08 11:22:57 +00:00
Mononaut
dc6dba416a Fix missing boost 2024-04-08 11:22:57 +00:00
Mononaut
c8e7cc773a Always use local data for pending/historical accelerations 2024-04-08 11:22:57 +00:00
Mononaut
efe43329a1 Support PREFER_LOCAL for /accelerations(/history) 2024-04-08 11:22:56 +00:00
Mononaut
3f97c17af2 Fetch block accelerations by height instead of hash 2024-04-08 11:22:56 +00:00
Mononaut
91493e8769 Roll back local acceleration data on reorg 2024-04-08 11:22:56 +00:00
softsimon
5f36cb88ab Merge pull request #4895 from mempool/hunicus/txacc-faq
Add note about accelerator waitlist to faq
2024-04-08 18:44:40 +09:00
Mononaut
a0ef635c92 Slightly wider sticky button 2024-04-08 09:36:45 +00:00
natsoni
d68904fec0 More contrast theme fixes 2024-04-08 18:32:59 +09:00
Mononaut
b679581cf2 Actually fix sticky button 2024-04-08 09:25:53 +00:00
softsimon
f7e6fa026d Merge pull request #4921 from mempool/mononaut/fix-accel-highlight
Fix new acceleration visualization color change
2024-04-08 18:04:23 +09:00
softsimon
96f16f1f2e Merge pull request #4914 from mempool/hunicus/why-no-confirm
Remove another faq reference to tx-acc
2024-04-08 18:01:34 +09:00
Mononaut
ad8fa8722f Fix new acceleration color change 2024-04-08 09:00:51 +00:00
wiz
e477f09cd5 Merge pull request #4920 from mempool/mononaut/fix-tx-autoscroll
Fix tx autoscroll
2024-04-08 17:59:54 +09:00
Mononaut
be5eb9ef70 Fix tx autoscroll 2024-04-08 08:51:35 +00:00
wiz
b952642570 Update staging URLs for prod Square SDK load 2024-04-08 17:12:38 +09:00
wiz
47cc74a351 Merge pull request #4913 from mempool/mononaut/simple-acceleration-mode
Simplified acceleration mode for mobile
2024-04-08 17:06:20 +09:00
wiz
cff572a104 Merge branch 'master' into mononaut/simple-acceleration-mode 2024-04-08 17:05:51 +09:00
Mononaut
48e16e64c2 Simple mode redesign w/ sticky button 2024-04-08 08:05:22 +00:00
wiz
46172836f1 Merge pull request #4915 from mempool/simon/fix-mobile-initial-zoom
Fix initial zoom behavior on mobile
2024-04-08 16:13:18 +09:00
wiz
eacb72a05b Merge pull request #4912 from mempool/mononaut/accelerated-fee-rates
Fix accelerated fee rates
2024-04-08 16:10:55 +09:00
softsimon
c1fefaab92 Pull from transifex 2024-04-08 16:01:26 +09:00
natsoni
f2f86457ee Fix dropdown menu position in Liquid 2024-04-08 15:53:51 +09:00
natsoni
c251b5831b Merge branch 'master' into natsoni/contrast-theme 2024-04-08 14:54:38 +09:00
softsimon
8c589d3000 Fix initial zoom behavior on mobile
fixes #4875
2024-04-08 14:54:33 +09:00
hunicus
ddc599f6b7 Remove another faq reference to tx-acc 2024-04-08 14:18:53 +09:00
hunicus
6822c3a04b Merge branch 'master' into hunicus/txacc-faq 2024-04-08 14:00:00 +09:00
Mononaut
aa0c70bd44 Simplified acceleration mode for mobile 2024-04-08 04:58:56 +00:00
hunicus
94f649b345 Add condition for officialmempoolspace 2024-04-08 13:50:48 +09:00
Mononaut
5e07e9dceb Fix accelerated fee rates 2024-04-08 03:03:26 +00:00
softsimon
91a8a8be34 Merge pull request #4911 from daweilv/master
fix(address): clicking on the Balance History chart within the /testnet/address/:id page would incorrectly navigate to /tx/:tx
2024-04-08 11:41:54 +09:00
natsoni
99e1890795 Fix footer to fit with theme selector 2024-04-08 10:50:59 +09:00
Metadavid
5583befbba Update daweilv.txt 2024-04-07 21:42:36 +08:00
Metadavid
c637055859 Update daweilv.txt 2024-04-07 21:20:42 +08:00
Metadavid
c3acfb8781 Update address-graph.component.ts
fix(address): Clicking on the Balance History chart within the /testnet/address/:id page under testnet now navigates to /testnet/tx/:tx instead of /tx/:tx.
2024-04-07 21:04:23 +08:00
Metadavid
fd073a7043 Add files via upload 2024-04-07 21:03:20 +08:00
nymkappa
bed00fbd41 [server] express server also listens on unix socket 2024-04-07 18:39:37 +09:00
softsimon
501b79fdce Italian and Spanish fineshed 2024-04-07 10:43:17 +09:00
softsimon
ed6af8f560 Merge pull request #4906 from mempool/mononaut/acceleration-harmony
Acceleration sound
2024-04-06 19:02:26 +09:00
Mononaut
32495736d4 such bright wow harmony 2024-04-06 09:58:07 +00:00
wiz
bff48b0a64 Merge pull request #4901 from mempool/nymkappa/clear-state
[accelerator] clear state after loading preview
2024-04-06 18:47:46 +09:00
softsimon
f6228240ba Transifex pull 2024-04-06 18:04:39 +09:00
softsimon
215c8a7ff4 Merge pull request #4902 from mempool/mononaut/disappearing-effective-rates
Fix disappearing effective/accelerated fee rates
2024-04-06 18:00:07 +09:00
softsimon
0f217bd753 Merge pull request #4903 from mempool/mononaut/bright-purple
Don't apply age tint to accelerated txs
2024-04-06 17:39:24 +09:00
natsoni
7e920f4bae Replace more hardcoded css 2024-04-06 17:14:53 +09:00
natsoni
cde3d878b1 Fix block overview graph on contrast theme 2024-04-06 15:59:42 +09:00
natsoni
621a6ea42d Fix rbf tree fade out 2024-04-06 15:59:36 +09:00
natsoni
cfc3615e64 Fix global footer selector css 2024-04-06 15:59:30 +09:00
natsoni
2f8d0d90cd Update theme-contrast.css 2024-04-06 15:59:26 +09:00
natsoni
c827953ca5 Fix remaining bugs from rebase 2024-04-06 15:59:07 +09:00
Mononaut
79dd263fb1 add high contrast theme 2024-04-06 15:56:53 +09:00
Mononaut
1ca05a029a add theme selector to main dashboard 2024-04-06 15:56:22 +09:00
Mononaut
4c205eb09d implement theme switching service 2024-04-06 15:55:47 +09:00
natsoni
ee92f6639a convert to CSS variables 2024-04-06 15:54:55 +09:00
Mononaut
7721f16f9f Don't apply age tint to accelerated txs 2024-04-06 03:44:58 +00:00
nymkappa
854222b8cc [accelerator] clear state after loading preview 2024-04-06 12:32:33 +09:00
Mononaut
0bc86541c6 Fix disappearing effective/accelerated fee rates 2024-04-06 03:22:56 +00:00
softsimon
c45111333d Fix space 2024-04-06 11:58:48 +09:00
softsimon
ba6fedc430 Merge pull request #4900 from mempool/nymkappa/fix-accel-preview-autoscroll
[accelerator] fix accel preview autoscroll
2024-04-05 21:06:57 +09:00
nymkappa
caa34fa8bb [accelerator] fix accel preview autoscroll 2024-04-05 20:59:15 +09:00
softsimon
fa7c0ab58e Pull from transifex 2024-04-05 20:46:37 +09:00
wiz
2543eb6861 Merge pull request #4899 from mempool/nymkappa/polish-button
[accelerator] fix label accelerate button
2024-04-05 20:45:27 +09:00
nymkappa
c4cf3363ee [accelerator] fix label accelerate button 2024-04-05 20:39:43 +09:00
softsimon
98d4efd509 Merge pull request #4889 from mempool/mononaut/non-esplora-inscription-goggles
Add support for inscription goggles for non-esplora backends
2024-04-05 18:28:07 +09:00
softsimon
a99e65ee48 Merge branch 'master' into mononaut/non-esplora-inscription-goggles 2024-04-05 18:28:01 +09:00
wiz
3fd4d24c93 Merge pull request #4897 from mempool/nymkappa/fix-preview-loading
[accelerator] fix spinner loading
2024-04-05 18:23:38 +09:00
nymkappa
55a564a5a8 [accelerator] fix spinner loading 2024-04-05 17:52:07 +09:00
softsimon
04f6490eeb Merge branch 'master' into mononaut/non-esplora-inscription-goggles 2024-04-05 17:48:55 +09:00
softsimon
b5026789d6 Merge pull request #4896 from mempool/nymkappa/fix-referrer
[accelator] fix referrer check
2024-04-05 17:38:47 +09:00
nymkappa
705e570cf5 [accelator] fix referrer check 2024-04-05 17:36:04 +09:00
softsimon
10a41fb0d1 Merge pull request #4804 from mempool/nymkappa/prepaid-acceleration
[accelerator] prepaid acceleration
2024-04-05 17:32:46 +09:00
hunicus
2a591646c3 Add note about accelerator waitlist to faq 2024-04-05 16:53:19 +09:00
softsimon
e55898b414 Merge pull request #4891 from mempool/mononaut/tx-details-stripes
Transaction details template refactor
2024-04-05 16:50:41 +09:00
Mononaut
2df476406d Transaction details template refactor 2024-04-05 07:46:48 +00:00
softsimon
4a05b35f2b Pulling from transifex 2024-04-05 16:46:23 +09:00
softsimon
3ad8e56c25 Accelerations i18n key fix 2024-04-05 16:30:26 +09:00
softsimon
c1704758fd Merge pull request #4893 from mempool/simon/build-target-es2020
Set build target to es2020
2024-04-05 16:26:38 +09:00
softsimon
fe580f2b2b Merge pull request #4894 from mempool/hunicus/esplora-docs
Switch blockstream/electrs for mempool/electrs in backend readme
2024-04-05 16:19:35 +09:00
hunicus
d82e482acc Update readme: blockstream/electrs for mempool/electrs 2024-04-05 15:59:16 +09:00
softsimon
9d6142dc79 Merge pull request #4892 from mempool/hunicus/memsplora
Switch blockstream/electrs for mempool/electrs in faq
2024-04-05 15:57:14 +09:00
softsimon
2dec83735b Set build target to es2020 2024-04-05 15:56:10 +09:00
softsimon
9f4204b815 Remove goggles i18n 2024-04-05 15:50:34 +09:00
hunicus
40f2c972d7 Update faq: blockstream/electrs for mempool/electrs 2024-04-05 15:50:15 +09:00
softsimon
eee2385d0d Merge pull request #4886 from mempool/nymkappa/mempool-goggle-tm
[copy] add missing trademark to Mempool Goggles
2024-04-05 14:48:43 +09:00
softsimon
492c652157 Merge pull request #4885 from mempool/natsoni/fix-side-nav-on-rtl
Fix side panel for rtl languages
2024-04-05 14:47:36 +09:00
softsimon
af8e060dc6 A few goggles i18n 2024-04-05 12:37:17 +09:00
softsimon
6a19b9058f Merge pull request #4888 from mempool/mononaut/tint
Goggles age tint by default
2024-04-05 12:36:24 +09:00
nymkappa
bcbd21b922 [acclerator] load square for prepaid acceleration 2024-04-05 11:16:48 +09:00
nymkappa
aac76d68b0 Merge branch 'master' into nymkappa/prepaid-acceleration 2024-04-05 11:14:16 +09:00
softsimon
25bb942e04 Fixing the other i18n 2024-04-04 21:55:14 +09:00
softsimon
8ac71f21d1 Updating i18n for accelerator preview 2024-04-04 21:48:16 +09:00
Mononaut
287559f028 Add support for inscription goggles for non-esplora backends 2024-04-04 12:32:03 +00:00
Mononaut
7fbb93cf41 Goggles age tint by default 2024-04-04 11:33:51 +00:00
nymkappa
7b76e93631 Update messages.ca.xlf 2024-04-04 18:26:38 +08:00
wiz
896c451c3e Merge pull request #4884 from mempool/nymkappa/about-page 2024-04-04 19:18:48 +09:00
natsoni
0874386001 Fix side panel for rtl languages 2024-04-04 19:00:04 +09:00
nymkappa
ce6808b3c4 [copy] add missing trademark to Mempool Goggles 2024-04-04 18:01:01 +09:00
softsimon
40fa1c5acb Some language string updates 2024-04-04 17:51:42 +09:00
nymkappa
7307990f6f [about page] enable sponsors on self hosted 2024-04-04 17:36:37 +09:00
softsimon
5104da500e Adding i18n to mempool goggles 2024-04-04 17:28:32 +09:00
softsimon
3a8c46bbed Merge pull request #4837 from mempool/mononaut/goggles-age-filter
Goggles age filter
2024-04-04 16:47:34 +09:00
Mononaut
fc5312549d Switch gradient toggle fee -> default 2024-04-04 07:42:31 +00:00
softsimon
c14e8797e2 Merge pull request #4879 from mempool/nymkappa/proxy-accel-endpoints
[accelerator] proxy acceleration api to prod
2024-04-04 16:20:27 +09:00
nymkappa
0f26940018 Merge branch 'master' into nymkappa/proxy-accel-endpoints 2024-04-04 16:01:17 +09:00
Mononaut
26227e2f3b New opacity-based age Goggles 2024-04-04 06:57:38 +00:00
Mononaut
dcf78fab06 Block visualization color-by-age mode 2024-04-04 06:57:37 +00:00
softsimon
0813592a6d Merge pull request #4882 from mempool/nymkappa/reset-pool-sha
[migration] reset mining pool sha to force refreshing
2024-04-04 15:20:04 +09:00
nymkappa
abdb27af3f [migration] reset mining pool sha to force refreshing 2024-04-04 14:27:50 +09:00
nymkappa
b421be3315 [accelerator] also forward headers 2024-04-04 14:04:12 +09:00
softsimon
b74fbee069 Merge pull request #4881 from mempool/nymkappa/proxy-sponsors
[about page] proxy community sponsors apis to prod, small refactor
2024-04-04 14:02:00 +09:00
nymkappa
dab9357b40 [about page] proxy community sponsors apis to prod, small refactor 2024-04-04 13:56:39 +09:00
softsimon
ccd89604c0 Merge pull request #4856 from mempool/hunicus/docs-links-alignment
Fix api-docs anchor link vertical alignment
2024-04-04 13:54:53 +09:00
Felipe Knorr Kuhn
cd135b7171 Merge branch 'master' into nymkappa/proxy-accel-endpoints 2024-04-04 13:14:54 +09:00
softsimon
7e16d550b0 Merge pull request #4880 from mempool/knorrium/enable_fiat_price_again
Enable fiat price conversion again
2024-04-04 13:14:24 +09:00
nymkappa
404079ef4e [accelerator] use config.MEMPOOL_SERVICES.API url 2024-04-04 13:10:32 +09:00
Felipe Knorr Kuhn
d13f78f046 Enable fiat price conversion again 2024-04-04 13:10:11 +09:00
nymkappa
60040c3914 [accelerator] proxy acceleration api to prod 2024-04-04 12:57:54 +09:00
softsimon
e408fbd8d6 Merge branch 'master' into hunicus/docs-links-alignment 2024-04-04 12:40:14 +09:00
softsimon
6931ecd468 Merge pull request #4878 from mempool/dependabot/npm_and_yarn/backend/multi-b37afcb6a9
Bump ws and @types/ws in /backend
2024-04-04 12:31:27 +09:00
dependabot[bot]
ed5d30ea5b Bump ws and @types/ws in /backend
Bumps [ws](https://github.com/websockets/ws) and [@types/ws](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws). These dependencies needed to be updated together.

Updates `ws` from 8.13.0 to 8.16.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.13.0...8.16.0)

Updates `@types/ws` from 8.5.5 to 8.5.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ws)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/ws"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-04 02:49:18 +00:00
wiz
ead7613579 Merge pull request #4877 from mempool/nymkappa/truncate-hashrate-difficulty
[migration] re-index hashrates and difficulty_adjustments
2024-04-04 11:05:16 +09:00
wiz
ca0613ec17 Merge pull request #4876 from mempool/knorrium/change_default_replication_settings
Update default settings for Docker
2024-04-04 11:05:04 +09:00
nymkappa
727208ff84 [migration] re-index hashrates and difficulty_adjustments 2024-04-04 10:57:54 +09:00
Felipe Knorr Kuhn
53c3de2af5 Update default settings for Docker 2024-04-04 10:49:32 +09:00
softsimon
3f50d57ed1 Fix test broke due to i18n key change 2024-04-03 21:44:48 +09:00
softsimon
55f9d0f875 Merge branch 'master' into hunicus/docs-links-alignment 2024-04-03 18:57:46 +09:00
softsimon
37cb9b0fe8 Merge pull request #4872 from mempool/natsoni/fix-fiat-selector-sorting
Sort currency by ticker in fiat selector
2024-04-03 18:57:24 +09:00
softsimon
a3b5f79094 Merge pull request #4871 from mempool/natsoni/fix-liquid-testnet
Fix Liquid testnet broken blocks
2024-04-03 18:55:55 +09:00
softsimon
1b1ffa7109 Transifex pull 04-03 2024-04-03 18:47:16 +09:00
natsoni
073578243c Fix fiat selector sorting 2024-04-03 16:34:56 +09:00
natsoni
e9c40692a6 Fix network switch mechanism 2024-04-03 16:09:30 +09:00
Felipe Knorr Kuhn
995a26b944 Merge branch 'master' into nymkappa/prepaid-acceleration 2024-04-02 21:53:01 +09:00
Mononaut
96ba7d0656 Add draft sip tx page 2024-04-02 05:28:03 +00:00
Mononaut
6852319e4d Improve sip API error handling 2024-04-02 05:26:27 +00:00
Mononaut
8bc1eaebc0 Sip set /block canonical url 2024-04-02 05:26:27 +00:00
Mononaut
eeefaa6374 Proof-of-concept "sipper" minimal pages for web crawlers 2024-04-02 05:26:24 +00:00
hunicus
fb6aec0afe Merge branch 'master' into hunicus/docs-links-alignment 2024-04-01 19:06:18 +09:00
hunicus
b8a48314c1 Fix api-docs anchor link vertical alignment 2024-04-01 18:53:38 +09:00
nymkappa
1eb52d8a35 [accelerator] fix redirection link 2024-03-21 19:08:48 +09:00
nymkappa
8fee195577 [accelerator] prepaid acceleration 2024-03-21 16:44:07 +09:00
592 changed files with 167059 additions and 91753 deletions

View File

@@ -35,7 +35,7 @@ jobs:
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
# Latest version available on this commit is 1.71.1
# Commit date is Aug 3, 2023
uses: dtolnay/rust-toolchain@dc6353516c68da0f06325f42ad880f76a5e77ec9
uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a
with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
@@ -257,7 +257,7 @@ jobs:
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet/*.spec.ts
cypress/e2e/testnet4/*.spec.ts
- module: "liquid"
spec: |
cypress/e2e/liquid/liquid.spec.ts

12
LICENSE
View File

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

View File

@@ -20,6 +20,7 @@
"@typescript-eslint/no-this-alias": 1,
"@typescript-eslint/no-var-requires": 1,
"@typescript-eslint/explicit-function-return-type": 1,
"@typescript-eslint/no-unused-vars": 1,
"no-console": 1,
"no-constant-condition": 1,
"no-dupe-else-if": 1,
@@ -32,6 +33,8 @@
"prefer-rest-params": 1,
"quotes": [1, "single", { "allowTemplateLiterals": true }],
"semi": 1,
"eqeqeq": 1
"curly": [1, "all"],
"eqeqeq": 1,
"no-trailing-spaces": 1
}
}

View File

@@ -103,7 +103,7 @@ In particular, make sure:
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
- the correct `BACKEND` is specified in `MEMPOOL`:
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
- "esplora" if you're using [Blockstream/electrs](https://github.com/Blockstream/electrs)
- "esplora" if you're using [mempool/electrs](https://github.com/mempool/electrs)
- "none" if you're not using any Electrum Server
### 6. Run Mempool Backend
@@ -181,7 +181,7 @@ Create a new wallet, if needed:
bitcoin-cli -regtest createwallet test
```
Load wallet (this command may take a while if you have lot of UTXOs):
Load wallet (this command may take a while if you have a lot of UTXOs):
```
bitcoin-cli -regtest loadwallet test
```
@@ -229,13 +229,13 @@ Generate block at regular interval (every 10 seconds in this example):
### Mining pools update
By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` is set to `false`).
By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` is set to `false`).
To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks.
You can enabled the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`.
You can enable the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` to `true` in your `mempool-config.json`.
When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionaly, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed.
When a `coinbase tag` or `coinbase address` change is detected, pool assignments for all relevant blocks (tagged to that pool or the `unknown` mining pool, starting from height 130635) are updated using the new criteria.
### Re-index tables

View File

@@ -24,18 +24,19 @@
"EXTERNAL_RETRY_INTERVAL": 0,
"USER_AGENT": "mempool",
"STDOUT_LOG_MIN_PRIORITY": "debug",
"AUTOMATIC_BLOCK_REINDEXING": false,
"AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"AUDIT": false,
"RUST_GBT": false,
"RUST_GBT": true,
"LIMIT_GBT": false,
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1,
"MAX_TRACKED_ADDRESSES": 100
"MAX_TRACKED_ADDRESSES": 100,
"UNIX_SOCKET_PATH": ""
},
"CORE_RPC": {
"HOST": "127.0.0.1",
@@ -58,7 +59,8 @@
"RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",
@@ -138,6 +140,8 @@
"ENABLED": false,
"AUDIT": false,
"AUDIT_START_HEIGHT": 774000,
"STATISTICS": false,
"STATISTICS_START_TIME": 1481932800,
"SERVERS": [
"list",
"of",
@@ -151,6 +155,7 @@
},
"FIAT_PRICE": {
"ENABLED": true,
"PAID": false,
"API_KEY": "your-api-key-from-freecurrencyapi.com"
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -7,9 +7,10 @@
"BLOCKS_SUMMARIES_INDEXING": true,
"GOGGLES_INDEXING": false,
"HTTP_PORT": 1,
"UNIX_SOCKET_PATH": "/mempool/socket/mempool-bitcoin-mainnet",
"SPAWN_CLUSTER_PROCS": 2,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"AUTOMATIC_BLOCK_REINDEXING": false,
"AUTOMATIC_POOLS_UPDATE": false,
"POLL_RATE_MS": 3,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CACHE_ENABLED": true,
@@ -59,7 +60,8 @@
"RETRY_UNIX_SOCKET_AFTER": 888,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
@@ -130,6 +132,8 @@
"ENABLED": false,
"AUDIT": false,
"AUDIT_START_HEIGHT": 774000,
"STATISTICS": false,
"STATISTICS_START_TIME": 1481932800,
"SERVERS": []
},
"MEMPOOL_SERVICES": {
@@ -143,6 +147,7 @@
},
"FIAT_PRICE": {
"ENABLED": true,
"PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
}
}

View File

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

View File

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

View File

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

View File

@@ -20,9 +20,10 @@ describe('Mempool Backend Config', () => {
BLOCKS_SUMMARIES_INDEXING: false,
GOGGLES_INDEXING: false,
HTTP_PORT: 8999,
UNIX_SOCKET_PATH: '',
SPAWN_CLUSTER_PROCS: 0,
API_URL_PREFIX: '/api/v1/',
AUTOMATIC_BLOCK_REINDEXING: false,
AUTOMATIC_POOLS_UPDATE: false,
POLL_RATE_MS: 2000,
CACHE_DIR: './cache',
CACHE_ENABLED: true,
@@ -62,6 +63,7 @@ describe('Mempool Backend Config', () => {
REQUEST_TIMEOUT: 10000,
FALLBACK_TIMEOUT: 5000,
FALLBACK: [],
MAX_BEHIND_TIP: 2,
});
expect(config.CORE_RPC).toStrictEqual({
@@ -134,6 +136,8 @@ describe('Mempool Backend Config', () => {
ENABLED: false,
AUDIT: false,
AUDIT_START_HEIGHT: 774000,
STATISTICS: false,
STATISTICS_START_TIME: 1481932800,
SERVERS: []
});
@@ -150,6 +154,7 @@ describe('Mempool Backend Config', () => {
expect(config.FIAT_PRICE).toStrictEqual({
ENABLED: true,
PAID: false,
API_KEY: '',
});
});

View File

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

View File

@@ -0,0 +1,87 @@
import { Application } from "express";
import config from "../config";
import axios from "axios";
import logger from "../logger";
class AboutRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'services/sponsors', async (req, res) => {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to fetch sponsors from ${url}. ${e}`, 'About Page');
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to fetch sponsor profile image from ${url}. ${e}`, 'About Page');
res.status(500).end();
}
})
;
}
}
export default new AboutRoutes();

View File

@@ -1,738 +0,0 @@
import logger from '../logger';
import { MempoolTransactionExtended } from '../mempool.interfaces';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
const BLOCK_WEIGHT_UNITS = 4_000_000;
const BLOCK_SIGOPS = 80_000;
const MAX_RELATIVE_GRAPH_SIZE = 200;
const BID_BOOST_WINDOW = 40_000;
const BID_BOOST_MIN_OFFSET = 10_000;
const BID_BOOST_MAX_OFFSET = 400_000;
type Acceleration = {
txid: string;
max_bid: number;
};
interface TxSummary {
txid: string; // txid of the current transaction
effectiveVsize: number; // Total vsize of the dependency tree
effectiveFee: number; // Total fee of the dependency tree in sats
ancestorCount: number; // Number of ancestors
}
export interface AccelerationInfo {
txSummary: TxSummary;
targetFeeRate: number; // target fee rate (recommended next block fee, or median fee for mined block)
nextBlockFee: number; // fee in sats required to be in the next block (using recommended next block fee, or median fee for mined block)
cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
}
interface GraphTx {
txid: string;
vsize: number;
weight: number;
fees: {
base: number; // in sats
};
depends: string[];
spentby: string[];
}
interface MempoolTx extends GraphTx {
ancestorcount: number;
ancestorsize: number;
fees: { // in sats
base: number;
ancestor: number;
};
ancestors: Map<string, MempoolTx>,
ancestorRate: number;
individualRate: number;
score: number;
}
class AccelerationCosts {
/**
* Takes a list of accelerations and verbose block data
* Returns the "fair" boost rate to charge accelerations
*
* @param accelerationsx
* @param verboseBlock
*/
public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number {
// Run GBT ourselves to calculate accurate effective fee rates
// the list of transactions comes from a mined block, so we already know everything fits within consensus limits
const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);
// initialize working maps for fast tx lookups
const accMap = {};
const txMap = {};
for (const acceleration of accelerations) {
accMap[acceleration.txid] = acceleration;
}
for (const tx of template) {
txMap[tx.txid] = tx;
}
// Identify and exclude accelerated and otherwise prioritized transactions
const excludeMap = {};
let totalWeight = 0;
let minAcceleratedPackage = Infinity;
let lastEffectiveRate = 0;
// Iterate over the mined template from bottom to top.
// Transactions should appear in ascending order of mining priority.
for (const blockTx of [...blockTxs].reverse()) {
const txid = blockTx.txid;
const tx = txMap[txid];
totalWeight += tx.weight;
const isAccelerated = accMap[txid] != null;
// If a cluster has a in-band effective fee rate than the previous cluster,
// it must have been prioritized out-of-band (in order to have a higher mining priority)
// so exclude from the analysis.
const isPrioritized = tx.effectiveFeePerVsize < lastEffectiveRate;
if (isPrioritized || isAccelerated) {
let packageWeight = 0;
// exclude this whole CPFP cluster
for (const clusterTxid of tx.cluster) {
packageWeight += txMap[clusterTxid].weight;
if (!excludeMap[clusterTxid]) {
excludeMap[clusterTxid] = true;
}
}
// keep track of the smallest accelerated CPFP cluster for later
if (isAccelerated) {
minAcceleratedPackage = Math.min(minAcceleratedPackage, packageWeight);
}
}
if (!isPrioritized) {
if (!isAccelerated) {
lastEffectiveRate = tx.effectiveFeePerVsize;
}
}
}
// The Bid Boost Rate is calculated by disregarding the bottom X weight units of the block,
// where X is the larger of BID_BOOST_MIN_OFFSET or the smallest accelerated package weight (the "offset"),
// then taking the average fee rate of the following BID_BOOST_WINDOW weight units
// (ignoring accelerated transactions and their ancestors).
//
// Transactions within the offset might pay less than the fair rate due to bin-packing effects
// But the average rate paid by the next chunk of non-accelerated transactions provides a good
// upper bound on the "next best rate" of alternatives to including the accelerated transactions
// (since, if there were any better options, they would have been included instead)
const spareWeight = BLOCK_WEIGHT_UNITS - totalWeight;
const windowOffset = Math.min(Math.max(minAcceleratedPackage, BID_BOOST_MIN_OFFSET, spareWeight), BID_BOOST_MAX_OFFSET);
const leftBound = windowOffset;
const rightBound = windowOffset + BID_BOOST_WINDOW;
let totalFeeInWindow = 0;
let totalWeightInWindow = Math.max(0, spareWeight - leftBound);
let txIndex = blockTxs.length - 1;
for (let offset = spareWeight; offset < BLOCK_WEIGHT_UNITS && txIndex >= 0; txIndex--) {
const txid = blockTxs[txIndex].txid;
const tx = txMap[txid];
if (excludeMap[txid]) {
// skip prioritized transactions and their ancestors
continue;
}
const left = offset;
const right = offset + tx.weight;
offset += tx.weight;
if (right < leftBound) {
// not within window yet
continue;
}
if (left > rightBound) {
// past window
break;
}
// count fees for weight units within the window
const overlapLeft = Math.max(leftBound, left);
const overlapRight = Math.min(rightBound, right);
const overlapUnits = overlapRight - overlapLeft;
totalFeeInWindow += (tx.effectiveFeePerVsize * (overlapUnits / 4));
totalWeightInWindow += overlapUnits;
}
if (totalWeightInWindow < BID_BOOST_WINDOW) {
// not enough un-prioritized transactions to calculate a fair rate
// just charge everyone their max bids
return Infinity;
}
// Divide the total fee by the size of the BID_BOOST_WINDOW in vbytes
const averageRate = totalFeeInWindow / (BID_BOOST_WINDOW / 4);
return averageRate;
}
/**
* Takes an accelerated mined txid and a target rate
* Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
*
* @param txid
* @param medianFeeRate
*/
public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
// Get same-block transaction ancestors
const allRelatives = this.getSameBlockRelatives(tx, transactions);
const relativesMap = this.initializeRelatives(allRelatives);
const rootTx = relativesMap.get(tx.txid) as MempoolTx;
// Calculate cost to boost
return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
}
/**
* Takes a raw transaction, and builds a graph of same-block relatives,
* and returns as a MempoolTx
*
* @param tx
*/
private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
for (const tx of transactions) {
blockTxs.set(tx.txid, tx);
for (const vin of tx.vin) {
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
}
}
const relatives: Map<string, GraphTx> = new Map();
const stack: string[] = [tx.txid];
// build set of same-block ancestors
while (stack.length > 0) {
const nextTxid = stack.pop();
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
if (!nextTx || relatives.has(nextTx.txid)) {
continue;
}
const mempoolTx = this.convertToGraphTx(nextTx);
mempoolTx.fees.base = nextTx.fee || 0;
mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[];
mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[];
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
if (txid) {
stack.push(txid);
}
}
relatives.set(mempoolTx.txid, mempoolTx);
}
return relatives;
}
/**
* Takes a raw transaction and converts it to MempoolTx format
* fee and ancestor data is initialized with dummy/null values
*
* @param tx
*/
private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx {
return {
txid: tx.txid,
vsize: Math.ceil(tx.weight / 4),
weight: tx.weight,
fees: {
base: 0, // dummy
},
depends: [], // dummy
spentby: [], //dummy
};
}
private convertGraphToMempoolTx(tx: GraphTx): MempoolTx {
return {
...tx,
fees: {
base: tx.fees.base,
ancestor: tx.fees.base,
},
ancestorcount: 1,
ancestorsize: Math.ceil(tx.weight / 4),
ancestors: new Map<string, MempoolTx>(),
ancestorRate: 0,
individualRate: 0,
score: 0,
};
}
/**
* Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
* Calculate the minimum set of transactions to fee-bump, their total vsize + fees
*
* @param tx
* @param ancestors
*/
private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo {
// add root tx to the ancestor map
relatives.set(tx.txid, tx);
// Check for high-sigop transactions (not supported)
relatives.forEach(entry => {
if (entry.vsize > Math.ceil(entry.weight / 4)) {
throw new Error(`high_sigop_tx`);
}
});
// Initialize individual & ancestor fee rates
relatives.forEach(entry => this.setAncestorScores(entry));
// Sort by descending ancestor score
let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
let includedInCluster: Map<string, MempoolTx> | null = null;
// While highest score >= targetFeeRate
let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
while (sortedRelatives.length && sortedRelatives[0].score && sortedRelatives[0].score >= targetFeeRate && maxIterations > 0) {
maxIterations--;
// Grab the highest scoring entry
const best = sortedRelatives.shift();
if (best) {
const cluster = new Map<string, MempoolTx>(best.ancestors?.entries() || []);
if (best.ancestors.has(tx.txid)) {
includedInCluster = cluster;
}
cluster.set(best.txid, best);
// Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
// and update scores, ancestor totals and dependencies for the survivors
this.removeAncestors(cluster, relatives);
// re-sort
sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
}
}
// sanity check for infinite loops / too many ancestors (should never happen)
if (maxIterations <= 0) {
logger.warn(`acceleration dependency calculation failed: calculateAccelerationAncestors loop exceeded ${MAX_RELATIVE_GRAPH_SIZE} iterations, unable to proceed`);
throw new Error('invalid_tx_dependencies');
}
let totalFee = tx.fees.ancestor;
// transaction is already CPFP-d above the target rate by some descendant
if (includedInCluster) {
let clusterSize = 0;
let clusterFee = 0;
includedInCluster.forEach(entry => {
clusterSize += entry.vsize;
clusterFee += entry.fees.base;
});
const clusterRate = clusterFee / clusterSize;
totalFee = Math.ceil(tx.ancestorsize * clusterRate);
}
// Whatever remains in the accelerated tx's dependencies needs to be boosted to the targetFeeRate
// Cost = (totalVsize * targetFeeRate) - totalFee
return {
txSummary: {
txid: tx.txid,
effectiveVsize: tx.ancestorsize,
effectiveFee: totalFee,
ancestorCount: tx.ancestorcount,
},
cost: Math.max(0, Math.ceil(tx.ancestorsize * targetFeeRate) - totalFee),
targetFeeRate,
nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
};
}
/**
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
* for each transaction.
*
* @param tx
* @param all
*/
private setAncestors(tx: MempoolTx, all: Map<string, MempoolTx>, visited: Map<string, Map<string, MempoolTx>>, depth: number = 0): Map<string, MempoolTx> {
// sanity check for infinite recursion / too many ancestors (should never happen)
if (depth >= 100) {
logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`);
throw new Error('invalid_tx_dependencies');
}
// initialize the ancestor map for this tx
tx.ancestors = new Map<string, MempoolTx>();
tx.depends.forEach(parentId => {
const parent = all.get(parentId);
if (parent) {
// add the parent
tx.ancestors?.set(parentId, parent);
// check for a cached copy of this parent's ancestors
let ancestors = visited.get(parent.txid);
if (!ancestors) {
// recursively fetch the parent's ancestors
ancestors = this.setAncestors(parent, all, visited, depth + 1);
}
// and add to this tx's map
ancestors.forEach((ancestor, ancestorId) => {
tx.ancestors?.set(ancestorId, ancestor);
});
}
});
visited.set(tx.txid, tx.ancestors);
return tx.ancestors;
}
/**
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
* by running setAncestors on each leaf, and caching intermediate results.
* then initializes ancestor data for each transaction
*
* @param all
*/
private initializeRelatives(all: Map<string, GraphTx>): Map<string, MempoolTx> {
const mempoolTxs = new Map<string, MempoolTx>();
all.forEach(entry => {
mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry));
});
const visited: Map<string, Map<string, MempoolTx>> = new Map();
const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
for (const leaf of leaves) {
this.setAncestors(leaf, mempoolTxs, visited);
}
mempoolTxs.forEach(entry => {
entry.ancestors?.forEach(ancestor => {
entry.ancestorcount++;
entry.ancestorsize += ancestor.vsize;
entry.fees.ancestor += ancestor.fees.base;
});
this.setAncestorScores(entry);
});
return mempoolTxs;
}
/**
* Remove a cluster of transactions from an in-mempool dependency graph
* and update the survivors' scores and ancestors
*
* @param cluster
* @param ancestors
*/
private removeAncestors(cluster: Map<string, MempoolTx>, all: Map<string, MempoolTx>): void {
// remove
cluster.forEach(tx => {
all.delete(tx.txid);
});
// update survivors
all.forEach(tx => {
cluster.forEach(remove => {
if (tx.ancestors?.has(remove.txid)) {
// remove as dependency
tx.ancestors.delete(remove.txid);
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
// update ancestor sizes and fees
tx.ancestorsize -= remove.vsize;
tx.fees.ancestor -= remove.fees.base;
}
});
// recalculate fee rates
this.setAncestorScores(tx);
});
}
/**
* Take a mempool transaction, and set the fee rates and ancestor score
*
* @param tx
*/
private setAncestorScores(tx: MempoolTx): void {
tx.individualRate = tx.fees.base / tx.vsize;
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
}
// Sort by descending score
private mempoolComparator(a, b): number {
return b.score - a.score;
}
}
export default new AccelerationCosts;
interface TemplateTransaction {
txid: string;
order: number;
weight: number;
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
sigops: number;
fee: number;
feeDelta: number;
ancestors: string[];
cluster: string[];
effectiveFeePerVsize: number;
}
interface MinerTransaction extends TemplateTransaction {
inputs: string[];
feePerVsize: number;
relativesSet: boolean;
ancestorMap: Map<string, MinerTransaction>;
children: Set<MinerTransaction>;
ancestorFee: number;
ancestorVsize: number;
ancestorSigops: number;
score: number;
used: boolean;
modified: boolean;
dependencyRate: number;
}
/*
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/
export function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
const auditPool: Map<string, MinerTransaction> = new Map();
const mempoolArray: MinerTransaction[] = [];
candidates.forEach(tx => {
// initializing everything up front helps V8 optimize property access later
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
const feePerVsize = (tx.fee / adjustedVsize);
auditPool.set(tx.txid, {
txid: tx.txid,
order: txidToOrdering(tx.txid),
fee: tx.fee,
feeDelta: 0,
weight: tx.weight,
adjustedVsize,
feePerVsize: feePerVsize,
effectiveFeePerVsize: feePerVsize,
dependencyRate: feePerVsize,
sigops: tx.sigops || 0,
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
relativesSet: false,
ancestors: [],
cluster: [],
ancestorMap: new Map<string, MinerTransaction>(),
children: new Set<MinerTransaction>(),
ancestorFee: 0,
ancestorVsize: 0,
ancestorSigops: 0,
score: 0,
used: false,
modified: false,
});
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
});
// set accelerated effective fee
for (const acceleration of accelerations) {
const tx = auditPool.get(acceleration.txid);
if (tx) {
tx.feeDelta = acceleration.max_bid;
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
tx.effectiveFeePerVsize = tx.feePerVsize;
tx.dependencyRate = tx.feePerVsize;
}
}
// Build relatives graph & calculate ancestor scores
for (const tx of mempoolArray) {
if (!tx.relativesSet) {
setRelatives(tx, auditPool);
}
}
// Sort by descending ancestor score
mempoolArray.sort(priorityComparator);
// Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score)
const blocks: number[][] = [];
let blockWeight = 0;
let blockSigops = 0;
const transactions: MinerTransaction[] = [];
let modified: MinerTransaction[] = [];
const overflow: MinerTransaction[] = [];
let failures = 0;
while (mempoolArray.length || modified.length) {
// skip invalid transactions
while (mempoolArray[0].used || mempoolArray[0].modified) {
mempoolArray.shift();
}
// Select best next package
let nextTx;
const nextPoolTx = mempoolArray[0];
const nextModifiedTx = modified[0];
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
nextTx = nextPoolTx;
mempoolArray.shift();
} else {
modified.shift();
if (nextModifiedTx) {
nextTx = nextModifiedTx;
}
}
if (nextTx && !nextTx?.used) {
// Check if the package fits into this block
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
const clusterTxids = sortedTxSet.map(tx => tx.txid);
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
const used: MinerTransaction[] = [];
while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop();
if (!ancestor) {
continue;
}
ancestor.used = true;
ancestor.usedBy = nextTx.txid;
// update this tx with effective fee rate & relatives data
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
ancestor.effectiveFeePerVsize = effectiveFeeRate;
}
ancestor.cluster = clusterTxids;
transactions.push(ancestor);
blockWeight += ancestor.weight;
blockSigops += ancestor.sigops;
used.push(ancestor);
}
// remove these as valid package ancestors for any descendants remaining in the mempool
if (used.length) {
used.forEach(tx => {
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
});
}
failures = 0;
} else {
// hold this package in an overflow list while we check for smaller options
overflow.push(nextTx);
failures++;
}
}
// this block is full
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
const queueEmpty = !mempoolArray.length && !modified.length;
if (exceededPackageTries || queueEmpty) {
break;
}
}
for (const tx of transactions) {
tx.ancestors = Object.values(tx.ancestorMap);
}
return transactions;
}
// traverse in-mempool ancestors
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives(
tx: MinerTransaction,
mempool: Map<string, MinerTransaction>,
): void {
for (const parent of tx.inputs) {
const parentTx = mempool.get(parent);
if (parentTx && !tx.ancestorMap?.has(parent)) {
tx.ancestorMap.set(parent, parentTx);
parentTx.children.add(tx);
// visit each node only once
if (!parentTx.relativesSet) {
setRelatives(parentTx, mempool);
}
parentTx.ancestorMap.forEach((ancestor) => {
tx.ancestorMap.set(ancestor.txid, ancestor);
});
}
};
tx.ancestorFee = (tx.fee + tx.feeDelta);
tx.ancestorVsize = tx.adjustedVsize || 0;
tx.ancestorSigops = tx.sigops || 0;
tx.ancestorMap.forEach((ancestor) => {
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
tx.ancestorVsize += ancestor.adjustedVsize;
tx.ancestorSigops += ancestor.sigops;
});
tx.score = tx.ancestorFee / tx.ancestorVsize;
tx.relativesSet = true;
}
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
// avoids recursion to limit call stack depth
function updateDescendants(
rootTx: MinerTransaction,
mempool: Map<string, MinerTransaction>,
modified: MinerTransaction[],
clusterRate: number,
): MinerTransaction[] {
const descendantSet: Set<MinerTransaction> = new Set();
// stack of nodes left to visit
const descendants: MinerTransaction[] = [];
let descendantTx: MinerTransaction | undefined;
rootTx.children.forEach(childTx => {
if (!descendantSet.has(childTx)) {
descendants.push(childTx);
descendantSet.add(childTx);
}
});
while (descendants.length) {
descendantTx = descendants.pop();
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
// remove tx as ancestor
descendantTx.ancestorMap.delete(rootTx.txid);
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
descendantTx.ancestorSigops -= rootTx.sigops;
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
if (!descendantTx.modified) {
descendantTx.modified = true;
modified.push(descendantTx);
}
// add this node's children to the stack
descendantTx.children.forEach(childTx => {
// visit each node only once
if (!descendantSet.has(childTx)) {
descendants.push(childTx);
descendantSet.add(childTx);
}
});
}
}
// return new, resorted modified list
return modified.sort(priorityComparator);
}
// Used to sort an array of MinerTransactions by descending ancestor score
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
if (b.score === a.score) {
// tie-break by txid for stability
return a.order - b.order;
} else {
return b.score - a.score;
}
}
// returns the most significant 4 bytes of the txid as an integer
function txidToOrdering(txid: string): number {
return parseInt(
txid.substring(62, 64) +
txid.substring(60, 62) +
txid.substring(58, 60) +
txid.substring(56, 58),
16
);
}

View File

@@ -0,0 +1,84 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import axios from 'axios';
import logger from '../../logger';
import mempool from '../mempool';
import AccelerationRepository from '../../repositories/AccelerationRepository';
class AccelerationRoutes {
private tag = 'Accelerator';
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this))
.post(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/estimate', this.$getAcceleratorEstimate.bind(this))
;
}
private async $getAcceleratorAccelerations(req: Request, res: Response): Promise<void> {
const accelerations = mempool.getAccelerations();
res.status(200).send(Object.values(accelerations));
}
private async $getAcceleratorAccelerationsHistory(req: Request, res: Response): Promise<void> {
const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null);
res.status(200).send(history.map(accel => ({
txid: accel.txid,
added: accel.added,
status: 'completed',
effectiveFee: accel.effective_fee,
effectiveVsize: accel.effective_vsize,
boostRate: accel.boost_rate,
boostCost: accel.boost_cost,
blockHeight: accel.height,
pools: [accel.pool],
})));
}
private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response): Promise<void> {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get aggregated acceleration history from ${url} in $getAcceleratorAccelerationsHistoryAggregated(), ${e}`, this.tag);
res.status(500).end();
}
}
private async $getAcceleratorAccelerationsStats(req: Request, res: Response): Promise<void> {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get acceleration stats from ${url} in $getAcceleratorAccelerationsStats(), ${e}`, this.tag);
res.status(500).end();
}
}
private async $getAcceleratorEstimate(req: Request, res: Response): Promise<void> {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.post(url, req.body, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get acceleration estimate from ${url} in $getAcceleratorEstimate(), ${e}`, this.tag);
res.status(500).end();
}
}
}
export default new AccelerationRoutes();

View File

@@ -0,0 +1,244 @@
import logger from '../../logger';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
import { GraphTx, getSameBlockRelatives, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from '../mini-miner';
const BLOCK_WEIGHT_UNITS = 4_000_000;
const MAX_RELATIVE_GRAPH_SIZE = 200;
const BID_BOOST_WINDOW = 40_000;
const BID_BOOST_MIN_OFFSET = 10_000;
const BID_BOOST_MAX_OFFSET = 400_000;
export type Acceleration = {
txid: string;
max_bid: number;
};
interface TxSummary {
txid: string; // txid of the current transaction
effectiveVsize: number; // Total vsize of the dependency tree
effectiveFee: number; // Total fee of the dependency tree in sats
ancestorCount: number; // Number of ancestors
}
export interface AccelerationInfo {
txSummary: TxSummary;
targetFeeRate: number; // target fee rate (recommended next block fee, or median fee for mined block)
nextBlockFee: number; // fee in sats required to be in the next block (using recommended next block fee, or median fee for mined block)
cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
}
class AccelerationCosts {
/**
* Takes a list of accelerations and verbose block data
* Returns the "fair" boost rate to charge accelerations
*
* @param accelerationsx
* @param verboseBlock
*/
public calculateBoostRate(accelerations: Acceleration[], blockTxs: MempoolTransactionExtended[]): number {
// Run GBT ourselves to calculate accurate effective fee rates
// the list of transactions comes from a mined block, so we already know everything fits within consensus limits
const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);
// initialize working maps for fast tx lookups
const accMap = {};
const txMap = {};
for (const acceleration of accelerations) {
accMap[acceleration.txid] = acceleration;
}
for (const tx of template) {
txMap[tx.txid] = tx;
}
// Identify and exclude accelerated and otherwise prioritized transactions
const excludeMap = {};
let totalWeight = 0;
let minAcceleratedPackage = Infinity;
let lastEffectiveRate = 0;
// Iterate over the mined template from bottom to top.
// Transactions should appear in ascending order of mining priority.
for (const blockTx of [...blockTxs].reverse()) {
const txid = blockTx.txid;
const tx = txMap[txid];
totalWeight += tx.weight;
const isAccelerated = accMap[txid] != null;
// If a cluster has a in-band effective fee rate than the previous cluster,
// it must have been prioritized out-of-band (in order to have a higher mining priority)
// so exclude from the analysis.
const isPrioritized = tx.effectiveFeePerVsize < lastEffectiveRate;
if (isPrioritized || isAccelerated) {
let packageWeight = 0;
// exclude this whole CPFP cluster
for (const clusterTxid of tx.cluster) {
packageWeight += txMap[clusterTxid].weight;
if (!excludeMap[clusterTxid]) {
excludeMap[clusterTxid] = true;
}
}
// keep track of the smallest accelerated CPFP cluster for later
if (isAccelerated) {
minAcceleratedPackage = Math.min(minAcceleratedPackage, packageWeight);
}
}
if (!isPrioritized) {
if (!isAccelerated) {
lastEffectiveRate = tx.effectiveFeePerVsize;
}
}
}
// The Bid Boost Rate is calculated by disregarding the bottom X weight units of the block,
// where X is the larger of BID_BOOST_MIN_OFFSET or the smallest accelerated package weight (the "offset"),
// then taking the average fee rate of the following BID_BOOST_WINDOW weight units
// (ignoring accelerated transactions and their ancestors).
//
// Transactions within the offset might pay less than the fair rate due to bin-packing effects
// But the average rate paid by the next chunk of non-accelerated transactions provides a good
// upper bound on the "next best rate" of alternatives to including the accelerated transactions
// (since, if there were any better options, they would have been included instead)
const spareWeight = BLOCK_WEIGHT_UNITS - totalWeight;
const windowOffset = Math.min(Math.max(minAcceleratedPackage, BID_BOOST_MIN_OFFSET, spareWeight), BID_BOOST_MAX_OFFSET);
const leftBound = windowOffset;
const rightBound = windowOffset + BID_BOOST_WINDOW;
let totalFeeInWindow = 0;
let totalWeightInWindow = Math.max(0, spareWeight - leftBound);
let txIndex = blockTxs.length - 1;
for (let offset = spareWeight; offset < BLOCK_WEIGHT_UNITS && txIndex >= 0; txIndex--) {
const txid = blockTxs[txIndex].txid;
const tx = txMap[txid];
if (excludeMap[txid]) {
// skip prioritized transactions and their ancestors
continue;
}
const left = offset;
const right = offset + tx.weight;
offset += tx.weight;
if (right < leftBound) {
// not within window yet
continue;
}
if (left > rightBound) {
// past window
break;
}
// count fees for weight units within the window
const overlapLeft = Math.max(leftBound, left);
const overlapRight = Math.min(rightBound, right);
const overlapUnits = overlapRight - overlapLeft;
totalFeeInWindow += (tx.effectiveFeePerVsize * (overlapUnits / 4));
totalWeightInWindow += overlapUnits;
}
if (totalWeightInWindow < BID_BOOST_WINDOW) {
// not enough un-prioritized transactions to calculate a fair rate
// just charge everyone their max bids
return Infinity;
}
// Divide the total fee by the size of the BID_BOOST_WINDOW in vbytes
const averageRate = totalFeeInWindow / (BID_BOOST_WINDOW / 4);
return averageRate;
}
/**
* Takes an accelerated mined txid and a target rate
* Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
*
* @param txid
* @param medianFeeRate
*/
public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
// Get same-block transaction ancestors
const allRelatives = getSameBlockRelatives(tx, transactions);
const relativesMap = initializeRelatives(allRelatives);
const rootTx = relativesMap.get(tx.txid) as GraphTx;
// Calculate cost to boost
return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
}
/**
* Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
* Calculate the minimum set of transactions to fee-bump, their total vsize + fees
*
* @param tx
* @param ancestors
*/
private calculateAccelerationAncestors(tx: GraphTx, relatives: Map<string, GraphTx>, targetFeeRate: number): AccelerationInfo {
// add root tx to the ancestor map
relatives.set(tx.txid, tx);
// Check for high-sigop transactions (not supported)
relatives.forEach(entry => {
if (entry.vsize > Math.ceil(entry.weight / 4)) {
throw new Error(`high_sigop_tx`);
}
});
// Initialize individual & ancestor fee rates
relatives.forEach(entry => setAncestorScores(entry));
// Sort by descending ancestor score
let sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator);
let includedInCluster: Map<string, GraphTx> | null = null;
// While highest score >= targetFeeRate
let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
while (sortedRelatives.length && sortedRelatives[0].score && sortedRelatives[0].score >= targetFeeRate && maxIterations > 0) {
maxIterations--;
// Grab the highest scoring entry
const best = sortedRelatives.shift();
if (best) {
const cluster = new Map<string, GraphTx>(best.ancestors?.entries() || []);
if (best.ancestors.has(tx.txid)) {
includedInCluster = cluster;
}
cluster.set(best.txid, best);
// Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
// and update scores, ancestor totals and dependencies for the survivors
removeAncestors(cluster, relatives);
// re-sort
sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator);
}
}
// sanity check for infinite loops / too many ancestors (should never happen)
if (maxIterations <= 0) {
logger.warn(`acceleration dependency calculation failed: calculateAccelerationAncestors loop exceeded ${MAX_RELATIVE_GRAPH_SIZE} iterations, unable to proceed`);
throw new Error('invalid_tx_dependencies');
}
let totalFee = tx.fees.ancestor;
// transaction is already CPFP-d above the target rate by some descendant
if (includedInCluster) {
let clusterSize = 0;
let clusterFee = 0;
includedInCluster.forEach(entry => {
clusterSize += entry.vsize;
clusterFee += entry.fees.base;
});
const clusterRate = clusterFee / clusterSize;
totalFee = Math.ceil(tx.ancestorsize * clusterRate);
}
// Whatever remains in the accelerated tx's dependencies needs to be boosted to the targetFeeRate
// Cost = (totalVsize * targetFeeRate) - totalFee
return {
txSummary: {
txid: tx.txid,
effectiveVsize: tx.ancestorsize,
effectiveFee: totalFee,
ancestorCount: tx.ancestorcount,
},
cost: Math.max(0, Math.ceil(tx.ancestorsize * targetFeeRate) - totalFee),
targetFeeRate,
nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
};
}
}
export default new AccelerationCosts;

View File

@@ -6,20 +6,22 @@ import rbfCache from './rbf-cache';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
: { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
}
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
const unseen: string[] = []; // present in the mined block, not in our mempool
const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse
const isDisplaced = {};
const isAccelerated = {};
let displacedWeight = 0;
let matchedWeight = 0;
let projectedWeight = 0;
@@ -32,6 +34,7 @@ class Audit {
inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid);
isAccelerated[tx.txid] = true;
}
}
// coinbase is always expected
@@ -75,10 +78,6 @@ class Audit {
let failures = 0;
let blockIndex = 1;
while (projectedBlocks[blockIndex] && failures < 500) {
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
index = 0;
blockIndex++;
}
const txid = projectedBlocks[blockIndex].transactionIds[index];
const tx = mempool[txid];
if (tx) {
@@ -102,6 +101,10 @@ class Audit {
logger.warn('projected transaction missing from mempool cache');
}
index++;
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
index = 0;
blockIndex++;
}
}
// mark unexpected transactions in the mined block as 'added'
@@ -113,11 +116,16 @@ class Audit {
} else {
if (rbfCache.has(tx.txid)) {
rbf.push(tx.txid);
} else if (!isDisplaced[tx.txid]) {
if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) {
unseen.push(tx.txid);
}
} else {
if (mempool[tx.txid]) {
prioritized.push(tx.txid);
if (isDisplaced[tx.txid]) {
added.push(tx.txid);
}
} else {
added.push(tx.txid);
unseen.push(tx.txid);
}
}
overflowWeight += tx.weight;
@@ -125,6 +133,24 @@ class Audit {
totalWeight += tx.weight;
}
// identify "prioritized" transactions
let lastEffectiveRate = 0;
// Iterate over the mined template from bottom to top (excluding the coinbase)
// Transactions should appear in ascending order of mining priority.
for (let i = transactions.length - 1; i > 0; i--) {
const blockTx = transactions[i];
// If a tx has a lower in-band effective fee rate than the previous tx,
// it must have been prioritized out-of-band (in order to have a higher mining priority)
// so exclude from the analysis.
if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) {
prioritized.push(blockTx.txid);
// accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference
} else if (!isAccelerated[blockTx.txid]) {
lastEffectiveRate = blockTx.effectiveFeePerVsize || 0;
}
}
// transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
let maxOverflowRate = 0;
@@ -165,6 +191,7 @@ class Audit {
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return {
unseen,
censored: Object.keys(isCensored),
added,
prioritized,

View File

@@ -1,4 +1,4 @@
import { IBitcoinApi } from './bitcoin-api.interface';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
@@ -22,11 +22,13 @@ export interface AbstractBitcoinApi {
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
startHealthChecks(): void;
getHealthStatus(): HealthCheckHost[];

View File

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

View File

@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import mempool from '../mempool';
@@ -107,8 +107,14 @@ class BitcoinApi implements AbstractBitcoinApi {
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
}
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.');
async $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
const verboseBlock: IBitcoinApi.VerboseBlock = await this.bitcoindClient.getBlock(hash, 2);
const transactions: IEsploraApi.Transaction[] = [];
for (const tx of verboseBlock.tx) {
const converted = await this.$convertTransaction(tx, true);
transactions.push(converted);
}
return transactions;
}
$getRawBlock(hash: string): Promise<Buffer> {
@@ -159,13 +165,21 @@ class BitcoinApi implements AbstractBitcoinApi {
const mp = mempool.getMempool();
for (const tx in mp) {
for (const vout of mp[tx].vout) {
if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
if (vout.scriptpubkey_address?.indexOf(prefix) === 0) {
found[vout.scriptpubkey_address] = '';
if (Object.keys(found).length >= 10) {
return Object.keys(found);
}
}
}
for (const vin of mp[tx].vin) {
if (vin.prevout?.scriptpubkey_address?.indexOf(prefix) === 0) {
found[vin.prevout?.scriptpubkey_address] = '';
if (Object.keys(found).length >= 10) {
return Object.keys(found);
}
}
}
}
return Object.keys(found);
}
@@ -174,6 +188,14 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.sendRawTransaction(rawTransaction);
}
async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> {
if (rawTransactions.length) {
return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined);
} else {
return [];
}
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return {
@@ -224,6 +246,11 @@ class BitcoinApi implements AbstractBitcoinApi {
return outspends;
}
async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> {
const txids = await this.$getTxIdsForBlock(blockhash);
return this.$getRawTransaction(txids[0]);
}
$getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);

View File

@@ -19,7 +19,7 @@ import bitcoinClient from './bitcoin-client';
import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
import { calculateCpfp } from '../cpfp';
import { calculateMempoolTxCpfp } from '../cpfp';
class BitcoinRoutes {
public initRoutes(app: Application) {
@@ -37,65 +37,12 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
.get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
@@ -109,6 +56,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/test', this.$testTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
@@ -212,13 +160,17 @@ class BitcoinRoutes {
descendants: tx.descendants || null,
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
sigops: tx.sigops,
fee: tx.fee,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined,
feeDelta: tx.feeDelta || undefined,
});
return;
}
const cpfpInfo = calculateCpfp(tx, mempool.getMempool());
const cpfpInfo = calculateMempoolTxCpfp(tx, mempool.getMempool());
res.json(cpfpInfo);
return;
@@ -412,6 +364,20 @@ class BitcoinRoutes {
}
}
private async $getBlockTxAuditSummary(req: Request, res: Response) {
try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
return res.status(404).send(`transaction audit not available`);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlocks(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
@@ -803,6 +769,19 @@ class BitcoinRoutes {
}
}
private async $testTransactions(req: Request, res: Response) {
try {
const rawTxs = Common.getTransactionsFromRequest(req);
const maxfeerate = parseFloat(req.query.maxfeerate as string);
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result);
} catch (e: any) {
res.setHeader('content-type', 'text/plain');
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
}
export default new BitcoinRoutes();

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
@@ -29,6 +29,11 @@ import websocketHandler from './websocket-handler';
import redisCache from './redis-cache';
import rbfCache from './rbf-cache';
import { calcBitsDifference } from './difficulty-adjustment';
import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -294,10 +299,12 @@ class Blocks {
extras.virtualSize = block.weight / 4.0;
if (coinbaseTx?.vout.length > 0) {
extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null;
extras.coinbaseAddresses = [...new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[])];
extras.coinbaseSignature = coinbaseTx.vout[0].scriptpubkey_asm ?? null;
extras.coinbaseSignatureAscii = transactionUtils.hex2ascii(coinbaseTx.vin[0].scriptsig) ?? null;
} else {
extras.coinbaseAddress = null;
extras.coinbaseAddresses = null;
extras.coinbaseSignature = null;
extras.coinbaseSignatureAscii = null;
}
@@ -369,8 +376,7 @@ class Blocks {
}
}
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter((address) => address);
const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter(address => address) as string[];
let pools: PoolTag[] = [];
if (config.DATABASE.ENABLED === true) {
@@ -379,26 +385,9 @@ class Blocks {
pools = poolsParser.miningPools;
}
for (let i = 0; i < pools.length; ++i) {
if (addresses.length) {
const poolAddresses: string[] = typeof pools[i].addresses === 'string' ?
JSON.parse(pools[i].addresses) : pools[i].addresses;
for (let y = 0; y < poolAddresses.length; y++) {
if (addresses.indexOf(poolAddresses[y]) !== -1) {
return pools[i];
}
}
}
const regexes: string[] = typeof pools[i].regexes === 'string' ?
JSON.parse(pools[i].regexes) : pools[i].regexes;
for (let y = 0; y < regexes.length; ++y) {
const regex = new RegExp(regexes[y], 'i');
const match = asciiScriptSig.match(regex);
if (match !== null) {
return pools[i];
}
}
const pool = poolsParser.matchBlockMiner(txMinerInfo.vin[0].scriptsig, addresses || [], pools);
if (pool) {
return pool;
}
if (config.DATABASE.ENABLED === true) {
@@ -451,7 +440,7 @@ class Blocks {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
if (cpfpSummary) {
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
@@ -582,8 +571,11 @@ class Blocks {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesWithVersion(0);
const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesWithVersion(0);
const targetSummaryVersion: number = 1;
const targetTemplateVersion: number = 1;
const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesBelowVersion(targetSummaryVersion);
const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesBelowVersion(targetTemplateVersion);
// nothing to do
if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) {
@@ -616,16 +608,24 @@ class Blocks {
for (let height = currentBlockHeight; height >= 0; height--) {
try {
let txs: TransactionExtended[] | null = null;
let txs: MempoolTransactionExtended[] | null = null;
if (unclassifiedBlocks[height]) {
const blockHash = unclassifiedBlocks[height];
// fetch transactions
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx)) || [];
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendMempoolTransaction(tx)) || [];
// add CPFP
const cpfpSummary = Common.calculateCpfp(height, txs, true);
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 1);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
if (!cpfpRepository.compareClusters(cpfpClusters, cpfpSummary.clusters)) {
// CPFP clusters changed - update the compact_cpfp tables
await CpfpRepository.$deleteClustersAt(height);
await this.$saveCpfp(blockHash, height, cpfpSummary);
}
}
await Common.sleep$(250);
}
if (unclassifiedTemplates[height]) {
@@ -651,7 +651,7 @@ class Blocks {
}
templateTxs.push(tx || templateTx);
}
const cpfpSummary = Common.calculateCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as TransactionExtended[], true);
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
@@ -689,6 +689,52 @@ class Blocks {
this.classifyingBlocks = false;
}
/**
* [INDEXING] Index missing coinbase addresses for all blocks
*/
public async $indexCoinbaseAddresses(): Promise<void> {
try {
// Get all indexed block hash
const unindexedBlocks = await blocksRepository.$getBlocksWithoutCoinbaseAddresses();
if (!unindexedBlocks?.length) {
return;
}
logger.info(`Indexing missing coinbase addresses for ${unindexedBlocks.length} blocks`);
// Logging
let count = 0;
let countThisRun = 0;
let timer = Date.now() / 1000;
const startedAt = Date.now() / 1000;
for (const { height, hash } of unindexedBlocks) {
// Logging
const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5) {
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = countThisRun / elapsedSeconds;
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
logger.debug(`Indexing coinbase addresses for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`);
timer = Date.now() / 1000;
countThisRun = 0;
}
const coinbaseTx = await bitcoinApi.$getCoinbaseTx(hash);
const addresses = new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[]);
await blocksRepository.$saveCoinbaseAddresses(hash, [...addresses]);
// Logging
count++;
countThisRun++;
}
logger.notice(`coinbase addresses indexing completed: indexed ${count} blocks`);
} catch (e) {
logger.err(`coinbase addresses indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e;
}
}
/**
* [INDEXING] Index all blocks metadata for the mining dashboard
*/
@@ -838,8 +884,11 @@ class Blocks {
} else {
this.currentBlockHeight++;
logger.debug(`New block found (#${this.currentBlockHeight})!`);
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
await chainTips.updateOrphanedBlocks();
// skip updating the orphan block cache if we've fallen behind the chain tip
if (this.currentBlockHeight >= blockHeightTip - 2) {
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
await chainTips.updateOrphanedBlocks();
}
}
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
@@ -856,7 +905,12 @@ class Blocks {
}
}
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
let accelerations = Object.values(mempool.getAccelerations());
if (accelerations?.length > 0) {
const pool = await this.$findBlockMiner(transactionUtils.stripCoinbaseTransaction(transactions[0]));
accelerations = accelerations.filter(a => a.pools.includes(pool.uniqueId));
}
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
@@ -872,18 +926,19 @@ class Blocks {
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
await HashratesRepository.$deleteLastEntries();
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
await AccelerationRepository.$deleteAccelerationsFrom(lastBlock.height - 10);
this.blocks = this.blocks.slice(0, -10);
this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`);
for (let i = 10; i >= 0; --i) {
const newBlock = await this.$indexBlock(lastBlock.height - i);
this.blocks.push(newBlock);
this.updateTimerProgress(timer, `reindexed block`);
let cpfpSummary;
let newCpfpSummary;
if (config.MEMPOOL.CPFP_INDEXING) {
cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
this.updateTimerProgress(timer, `reindexed block cpfp`);
}
await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height);
this.updateTimerProgress(timer, `reindexed block summary`);
}
await mining.$indexDifficultyAdjustments();
@@ -932,7 +987,7 @@ class Blocks {
// start async callbacks
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions));
if (block.height % 2016 === 0) {
if (Common.indexingEnabled()) {
@@ -974,6 +1029,9 @@ class Blocks {
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
}
blockSummary.transactions.forEach(tx => {
delete tx.acc;
});
this.blockSummaries.push(blockSummary);
if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
@@ -1111,12 +1169,13 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = tx.flags || Common.getTransactionFlags(tx);
flags = Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
return {
txid: tx.txid,
time: tx.firstSeen,
fee: tx.fee || 0,
vsize: tx.vsize,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
@@ -1125,7 +1184,7 @@ class Blocks {
};
}),
};
summaryVersion = 1;
summaryVersion = cpfpSummary.version;
} else {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
@@ -1250,6 +1309,7 @@ class Blocks {
utxoset_size: block.extras.utxoSetSize ?? null,
coinbase_raw: block.extras.coinbaseRaw ?? null,
coinbase_address: block.extras.coinbaseAddress ?? null,
coinbase_addresses: block.extras.coinbaseAddresses ?? null,
coinbase_signature: block.extras.coinbaseSignature ?? null,
coinbase_signature_ascii: block.extras.coinbaseSignatureAscii ?? null,
pool_slug: block.extras.pool.slug ?? null,
@@ -1319,6 +1379,14 @@ class Blocks {
}
}
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
} else {
return null;
}
}
public getLastDifficultyAdjustmentTime(): number {
return this.lastDifficultyAdjustmentTime;
}
@@ -1335,11 +1403,11 @@ class Blocks {
return this.currentBlockHeight;
}
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise<CpfpSummary | null> {
let transactions = txs;
if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') {
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
}
if (!transactions) {
const block = await bitcoinClient.getBlock(hash, 2);
@@ -1351,7 +1419,7 @@ class Blocks {
}
if (transactions?.length != null) {
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
const summary = calculateFastBlockCpfp(height, transactions);
await this.$saveCpfp(hash, height, summary);

View File

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

View File

@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { Request } from 'express';
import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net';
@@ -258,9 +258,15 @@ export class Common {
let opreturnCount = 0;
for (const vout of tx.vout) {
// scriptpubkey
if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
// (non-standard output type)
return true;
} else if (vout.scriptpubkey_type === 'unknown') {
// undefined segwit version/length combinations are actually standard in outputs
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951
if (vout.scriptpubkey.startsWith('00') || !this.isWitnessProgram(vout.scriptpubkey)) {
return true;
}
} else if (vout.scriptpubkey_type === 'multisig') {
if (!DEFAULT_PERMIT_BAREMULTISIG) {
// bare-multisig
@@ -286,7 +292,7 @@ export class Common {
dustSize += getVarIntLength(dustSize);
// add value size
dustSize += 8;
if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) {
if (Common.isWitnessProgram(vout.scriptpubkey)) {
dustSize += 67;
} else {
dustSize += 148;
@@ -308,6 +314,27 @@ export class Common {
return false;
}
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
static isWitnessProgram(scriptpubkey: string): false | { version: number, program: string } {
if (scriptpubkey.length < 8 || scriptpubkey.length > 84) {
return false;
}
const version = parseInt(scriptpubkey.slice(0,2), 16);
if (version !== 0 && version < 0x51 || version > 0x60) {
return false;
}
const push = parseInt(scriptpubkey.slice(2,4), 16);
if (push + 2 === (scriptpubkey.length / 2)) {
return {
version: version ? version - 0x50 : 0,
program: scriptpubkey.slice(4),
};
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;
@@ -373,16 +400,34 @@ export class Common {
].includes(pubkey);
}
static isInscription(vin, flags): bigint {
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
// script spends have more than one witness item, not counting the annex (if present)
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
// the script itself is the second-to-last witness item, not counting the annex
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
if (asm?.includes('OP_0 OP_IF')) {
flags |= TransactionFlags.inscription;
}
}
return flags;
}
static getTransactionFlags(tx: TransactionExtended): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
flags &= ~TransactionFlags.cpfp_child;
if (tx.ancestors?.length) {
flags |= TransactionFlags.cpfp_child;
}
flags &= ~TransactionFlags.cpfp_parent;
if (tx.descendants?.length) {
flags |= TransactionFlags.cpfp_parent;
}
flags &= ~TransactionFlags.replacement;
if (tx.replacement) {
flags |= TransactionFlags.replacement;
}
@@ -409,30 +454,30 @@ export class Common {
if (vin.sequence < 0xfffffffe) {
rbf = true;
}
switch (vin.prevout?.scriptpubkey_type) {
case 'p2pk': flags |= TransactionFlags.p2pk; break;
case 'multisig': flags |= TransactionFlags.p2ms; break;
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
case 'p2sh': flags |= TransactionFlags.p2sh; break;
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
case 'v1_p2tr': {
if (!vin.witness?.length) {
throw new Error('Taproot input missing witness data');
}
flags |= TransactionFlags.p2tr;
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
// script spends have more than one witness item, not counting the annex (if present)
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
// the script itself is the second-to-last witness item, not counting the annex
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
if (asm?.includes('OP_0 OP_IF')) {
flags |= TransactionFlags.inscription;
if (vin.prevout?.scriptpubkey_type) {
switch (vin.prevout?.scriptpubkey_type) {
case 'p2pk': flags |= TransactionFlags.p2pk; break;
case 'multisig': flags |= TransactionFlags.p2ms; break;
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
case 'p2sh': flags |= TransactionFlags.p2sh; break;
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
case 'v1_p2tr': {
flags |= TransactionFlags.p2tr;
if (vin.witness?.length) {
flags = Common.isInscription(vin, flags);
}
} break;
}
} else {
// no prevouts, optimistically check witness-bearing inputs
if (vin.witness?.length >= 2) {
try {
flags = Common.isInscription(vin, flags);
} catch {
// witness script parsing will fail if this isn't really a taproot output
}
} break;
}
}
// sighash flags
@@ -764,96 +809,6 @@ export class Common {
}
}
static calculateCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
const txMap: { [txid: string]: TransactionExtended } = {};
// initialize the txMap
for (const tx of transactions) {
txMap[tx.txid] = tx;
}
// reverse pass to identify CPFP clusters
for (let i = transactions.length - 1; i >= 0; i--) {
const tx = transactions[i];
if (!ancestors[tx.txid]) {
let totalFee = 0;
let totalVSize = 0;
clusterTxs.forEach(tx => {
totalFee += tx?.fee || 0;
totalVSize += (tx.weight / 4);
});
const effectiveFeePerVsize = totalFee / totalVSize;
let cluster: CpfpCluster;
if (clusterTxs.length > 1) {
cluster = {
root: clusterTxs[0].txid,
height,
txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
effectiveFeePerVsize,
};
clusters.push(cluster);
}
clusterTxs.forEach(tx => {
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
if (cluster) {
clusterMap[tx.txid] = cluster;
}
});
// reset working vars
clusterTxs = [];
ancestors = {};
}
clusterTxs.push(tx);
tx.vin.forEach(vin => {
ancestors[vin.txid] = true;
});
}
// forward pass to enforce ancestor rate caps
for (const tx of transactions) {
let minAncestorRate = tx.effectiveFeePerVsize;
for (const vin of tx.vin) {
if (txMap[vin.txid]?.effectiveFeePerVsize) {
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
}
}
// check rounded values to skip cases with almost identical fees
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
tx.effectiveFeePerVsize = minAncestorRate;
if (!clusterMap[tx.txid]) {
// add a single-tx cluster to record the dependent rate
const cluster = {
root: tx.txid,
height,
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
effectiveFeePerVsize: minAncestorRate,
};
clusterMap[tx.txid] = cluster;
clusters.push(cluster);
} else {
// update the existing cluster with the dependent rate
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
}
}
}
if (saveRelatives) {
for (const cluster of clusters) {
cluster.txs.forEach((member, index) => {
txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse();
txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse();
txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize;
});
}
}
return {
transactions,
clusters,
};
}
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats {
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
@@ -861,9 +816,10 @@ export class Common {
let medianFee = 0;
let medianWeight = 0;
// calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions
const leftBound = 1995000;
const rightBound = 2005000;
// calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of transactions
const halfWidth = config.MEMPOOL.BLOCK_WEIGHT_UNITS / 800;
const leftBound = Math.floor((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) - halfWidth);
const rightBound = Math.ceil((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) + halfWidth);
for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) {
const left = weightCount;
const right = weightCount + sortedTxs[i].weight;
@@ -930,6 +886,33 @@ export class Common {
return this.validateTransactionHex(matches[1].toLowerCase());
}
static getTransactionsFromRequest(req: Request, limit: number = 25): string[] {
if (!Array.isArray(req.body) || req.body.some(hex => typeof hex !== 'string')) {
throw Object.assign(new Error('Invalid request body (should be an array of hexadecimal strings)'), { code: -1 });
}
if (limit && req.body.length > limit) {
throw Object.assign(new Error('Exceeded maximum of 25 transactions'), { code: -1 });
}
const txs = req.body;
return txs.map(rawTx => {
// Support both upper and lower case hex
// Support both txHash= Form and direct API POST
const reg = /^((?:[a-fA-F0-9]{2})+)$/;
const matches = reg.exec(rawTx);
if (!matches || !matches[1]) {
throw Object.assign(new Error('Invalid hex string'), { code: -2 });
}
// Guaranteed to be a hex string of multiple of 2
// Guaranteed to be lower case
// Guaranteed to pass validation (see function below)
return this.validateTransactionHex(matches[1].toLowerCase());
});
}
private static validateTransactionHex(txhex: string): string {
// Do not mutate txhex

View File

@@ -1,29 +1,174 @@
import { CpfpInfo, MempoolTransactionExtended } from '../mempool.interfaces';
import { Ancestor, CpfpCluster, CpfpInfo, CpfpSummary, MempoolTransactionExtended, TransactionExtended } from '../mempool.interfaces';
import { GraphTx, convertToGraphTx, expandRelativesGraph, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from './mini-miner';
import memPool from './mempool';
import { Acceleration } from './acceleration/acceleration';
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
const MAX_GRAPH_SIZE = 50; // the maximum number of in-mempool relatives to consider
const MAX_CLUSTER_ITERATIONS = 100;
interface GraphTx extends MempoolTransactionExtended {
depends: string[];
spentby: string[];
ancestorMap: Map<string, GraphTx>;
fees: {
base: number;
ancestor: number;
export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
const txMap: { [txid: string]: TransactionExtended } = {};
// initialize the txMap
for (const tx of transactions) {
txMap[tx.txid] = tx;
}
// reverse pass to identify CPFP clusters
for (let i = transactions.length - 1; i >= 0; i--) {
const tx = transactions[i];
if (!ancestors[tx.txid]) {
let totalFee = 0;
let totalVSize = 0;
clusterTxs.forEach(tx => {
totalFee += tx?.fee || 0;
totalVSize += (tx.weight / 4);
});
const effectiveFeePerVsize = totalFee / totalVSize;
let cluster: CpfpCluster;
if (clusterTxs.length > 1) {
cluster = {
root: clusterTxs[0].txid,
height,
txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
effectiveFeePerVsize,
};
clusters.push(cluster);
}
clusterTxs.forEach(tx => {
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
if (cluster) {
clusterMap[tx.txid] = cluster;
}
});
// reset working vars
clusterTxs = [];
ancestors = {};
}
clusterTxs.push(tx);
tx.vin.forEach(vin => {
ancestors[vin.txid] = true;
});
}
// forward pass to enforce ancestor rate caps
for (const tx of transactions) {
let minAncestorRate = tx.effectiveFeePerVsize;
for (const vin of tx.vin) {
if (txMap[vin.txid]?.effectiveFeePerVsize) {
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
}
}
// check rounded values to skip cases with almost identical fees
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
tx.effectiveFeePerVsize = minAncestorRate;
if (!clusterMap[tx.txid]) {
// add a single-tx cluster to record the dependent rate
const cluster = {
root: tx.txid,
height,
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
effectiveFeePerVsize: minAncestorRate,
};
clusterMap[tx.txid] = cluster;
clusters.push(cluster);
} else {
// update the existing cluster with the dependent rate
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
}
}
}
if (saveRelatives) {
for (const cluster of clusters) {
cluster.txs.forEach((member, index) => {
txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse();
txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse();
txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize;
});
}
}
return {
transactions,
clusters,
version: 1,
};
}
export function calculateGoodBlockCpfp(height: number, transactions: MempoolTransactionExtended[], accelerations: Acceleration[]): CpfpSummary {
const txMap: { [txid: string]: MempoolTransactionExtended } = {};
for (const tx of transactions) {
txMap[tx.txid] = tx;
}
const template = makeBlockTemplate(transactions, accelerations, 1, Infinity, Infinity);
const clusters = new Map<string, string[]>();
for (const tx of template) {
const cluster = tx.cluster || [];
const root = cluster.length ? cluster[cluster.length - 1] : null;
if (cluster.length > 1 && root && !clusters.has(root)) {
clusters.set(root, cluster);
}
txMap[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
}
const clusterArray: CpfpCluster[] = [];
for (const cluster of clusters.values()) {
for (const txid of cluster) {
const mempoolTx = txMap[txid];
if (mempoolTx) {
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let matched = false;
cluster.forEach(relativeTxid => {
if (relativeTxid === txid) {
matched = true;
} else {
const relative = {
txid: relativeTxid,
fee: txMap[relativeTxid].fee,
weight: (txMap[relativeTxid].adjustedVsize * 4) || txMap[relativeTxid].weight,
};
if (matched) {
descendants.push(relative);
} else {
ancestors.push(relative);
}
}
});
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
Object.assign(mempoolTx, { ancestors, descendants, bestDescendant: null, cpfpChecked: true });
}
}
const root = cluster[cluster.length - 1];
clusterArray.push({
root: root,
height,
txs: cluster.reverse().map(txid => ({
txid,
fee: txMap[txid].fee,
weight: (txMap[txid].adjustedVsize * 4) || txMap[txid].weight,
})),
effectiveFeePerVsize: txMap[root].effectiveFeePerVsize,
});
}
return {
transactions: transactions.map(tx => txMap[tx.txid]),
clusters: clusterArray,
version: 2,
};
ancestorcount: number;
ancestorsize: number;
ancestorRate: number;
individualRate: number;
score: number;
}
/**
* 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)
*/
export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
tx.cpfpDirty = false;
return {
@@ -32,30 +177,31 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid:
descendants: tx.descendants || [],
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
sigops: tx.sigops,
fee: tx.fee,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
};
}
const ancestorMap = new Map<string, GraphTx>();
const graphTx = mempoolToGraphTx(tx);
const graphTx = convertToGraphTx(tx, memPool.getSpendMap());
ancestorMap.set(tx.txid, graphTx);
const allRelatives = expandRelativesGraph(mempool, ancestorMap);
const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap());
const relativesMap = initializeRelatives(allRelatives);
const cluster = calculateCpfpCluster(tx.txid, relativesMap);
let totalVsize = 0;
let totalFee = 0;
for (const tx of cluster.values()) {
totalVsize += tx.adjustedVsize;
totalFee += tx.fee;
totalVsize += tx.vsize;
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.ancestorMap.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestorMap.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
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;
@@ -70,88 +216,12 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid:
descendants: tx.descendants || [],
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
sigops: tx.sigops,
fee: tx.fee,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
};
}
function mempoolToGraphTx(tx: MempoolTransactionExtended): GraphTx {
return {
...tx,
depends: tx.vin.map(v => v.txid),
spentby: tx.vout.map((v, i) => memPool.getFromSpendMap(tx.txid, i)).map(tx => tx?.txid).filter(txid => txid != null) as string[],
ancestorMap: new Map(),
fees: {
base: tx.fee,
ancestor: tx.fee,
},
ancestorcount: 1,
ancestorsize: tx.adjustedVsize,
ancestorRate: 0,
individualRate: 0,
score: 0,
};
}
/**
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
*/
function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>): Map<string, GraphTx> {
const relatives: Map<string, GraphTx> = new Map();
const stack: GraphTx[] = Array.from(ancestors.values());
while (stack.length > 0) {
if (relatives.size > MAX_GRAPH_SIZE) {
return relatives;
}
const nextTx = stack.pop();
if (!nextTx) {
continue;
}
relatives.set(nextTx.txid, nextTx);
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
if (relatives.has(relativeTxid)) {
// already processed this tx
continue;
}
let mempoolTx = ancestors.get(relativeTxid);
if (!mempoolTx && mempool[relativeTxid]) {
mempoolTx = mempoolToGraphTx(mempool[relativeTxid]);
}
if (mempoolTx) {
stack.push(mempoolTx);
}
}
}
return relatives;
}
/**
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
* by running setAncestors on each leaf, and caching intermediate results.
* then initializes ancestor data for each transaction
*
* @param all
*/
function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
const visited: Map<string, Map<string, GraphTx>> = new Map();
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
for (const leaf of leaves) {
setAncestors(leaf, mempoolTxs, visited);
}
mempoolTxs.forEach(entry => {
entry.ancestorMap?.forEach(ancestor => {
entry.ancestorcount++;
entry.ancestorsize += ancestor.adjustedVsize;
entry.fees.ancestor += ancestor.fees.base;
});
setAncestorScores(entry);
});
return mempoolTxs;
}
/**
* Given a root transaction and a list of in-mempool ancestors,
* Calculate the CPFP cluster
@@ -172,10 +242,10 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator);
// Iterate until we reach a cluster that includes our target tx
let maxIterations = MAX_GRAPH_SIZE;
let maxIterations = MAX_CLUSTER_ITERATIONS;
let best = sortedRelatives.shift();
let bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestorMap.has(tx.txid)) && maxIterations > 0) {
let bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestors.has(tx.txid)) && maxIterations > 0) {
maxIterations--;
if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) {
break;
@@ -190,7 +260,7 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
// Grab the next highest scoring entry
best = sortedRelatives.shift();
if (best) {
bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
bestCluster.set(best?.txid, best);
}
}
@@ -199,88 +269,4 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
bestCluster.set(tx.txid, tx);
return bestCluster;
}
/**
* Remove a cluster of transactions from an in-mempool dependency graph
* and update the survivors' scores and ancestors
*
* @param cluster
* @param ancestors
*/
function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
// remove
cluster.forEach(tx => {
all.delete(tx.txid);
});
// update survivors
all.forEach(tx => {
cluster.forEach(remove => {
if (tx.ancestorMap?.has(remove.txid)) {
// remove as dependency
tx.ancestorMap.delete(remove.txid);
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
// update ancestor sizes and fees
tx.ancestorsize -= remove.adjustedVsize;
tx.fees.ancestor -= remove.fees.base;
}
});
// recalculate fee rates
setAncestorScores(tx);
});
}
/**
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
* for each transaction.
*
* @param tx
* @param all
*/
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
// sanity check for infinite recursion / too many ancestors (should never happen)
if (depth > MAX_GRAPH_SIZE) {
return tx.ancestorMap;
}
// initialize the ancestor map for this tx
tx.ancestorMap = new Map<string, GraphTx>();
tx.depends.forEach(parentId => {
const parent = all.get(parentId);
if (parent) {
// add the parent
tx.ancestorMap?.set(parentId, parent);
// check for a cached copy of this parent's ancestors
let ancestors = visited.get(parent.txid);
if (!ancestors) {
// recursively fetch the parent's ancestors
ancestors = setAncestors(parent, all, visited, depth + 1);
}
// and add to this tx's map
ancestors.forEach((ancestor, ancestorId) => {
tx.ancestorMap?.set(ancestorId, ancestor);
});
}
});
visited.set(tx.txid, tx.ancestorMap);
return tx.ancestorMap;
}
/**
* Take a mempool transaction, and set the fee rates and ancestor score
*
* @param tx
*/
function setAncestorScores(tx: GraphTx): GraphTx {
tx.individualRate = (tx.fees.base * 100_000_000) / tx.adjustedVsize;
tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize;
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
return tx;
}
// Sort by descending score
function mempoolComparator(a: GraphTx, b: GraphTx): number {
return b.score - a.score;
}

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 76;
private static currentVersion = 81;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -652,6 +652,13 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `prices` ADD `THB` float DEFAULT "-1"');
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
if (isBitcoin === true) {
await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
}
await this.updateToSchemaVersion(75);
}
@@ -659,6 +666,40 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(76);
}
if (databaseSchemaVersion < 77 && config.MEMPOOL.NETWORK === 'mainnet') {
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
await this.updateToSchemaVersion(77);
}
if (databaseSchemaVersion < 78) {
await this.$executeQuery('ALTER TABLE `prices` CHANGE `time` `time` datetime NOT NULL');
await this.updateToSchemaVersion(78);
}
if (databaseSchemaVersion < 79 && config.MEMPOOL.NETWORK === 'mainnet') {
// Clear bad data
await this.$executeQuery(`TRUNCATE accelerations`);
this.uniqueLog(logger.notice, `'accelerations' table has been truncated`);
await this.$executeQuery(`
UPDATE state
SET number = 0
WHERE name = 'last_acceleration_block'
`);
await this.updateToSchemaVersion(79);
}
if (databaseSchemaVersion < 80) {
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
await this.updateToSchemaVersion(80);
}
if (databaseSchemaVersion < 81 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81);
}
}
/**

View File

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

View File

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

View File

@@ -48,6 +48,14 @@ class NodesRoutes {
'032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470',
'022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421',
'02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680',
'02b36a324fa2dd3af2a63ac65f241907882829bed5002b4e14171d25c219e0d470',
'0231b6e8f21f9f6c057f6bf8a812f79e396ee16a66ece91939a1576ce9fb9e87a5',
'034b6aac206bffcbd651b7ead1ab8a0991c945dfafe19ff27dcdeadc6843ebd15c',
'039c065f7e344acd969ebdd4a94550915b6f24e8782ae2be540bb96c8a4fcfb86b',
'03d9f9f4803fc75920f14dd13d83fbecc53229a65d4ee4cd2d86fdf211f7337576',
'0357fe48c4dece744f70865eda66e396aab5d05e09e1145cd3b7da83f11446d4cf',
'02bca4d642eda631f2c8659758e2a2868e518b93503f2bfcd767749c6530a10679',
'03f32c99c0bb9f62dae53671d1d300565773455248f34134cc02779b881561174e',
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
@@ -60,6 +68,14 @@ class NodesRoutes {
'039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205',
'033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18',
'029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584',
'038eb09bed4532ff36d12acc1279f55cbe8d95212d19f809e057bb50de00051fba',
'027b7c0278366a0268e8bd0072b14539f6cb455a7bd588ae22d888bed541f65311',
'02f4dd78f6eda8838029b2cdbaaea6e875e2fa373cd348ee41a7c1bb177d3fca66',
'036b3fb692da214a3edaac5b67903b958f5ccd8712e09aa61b67ea7acfd94b40c2',
'023bc8915d308e0b65f8de6867f95960141372436fce3edad5cec3f364d6ac948f',
'0341690503ef21d0e203dddd9e62646380d0dfc32c499e055e7f698b9064d1c736',
'0355d573805c018a37a5b2288378d70e9b5b438f7394abd6f467cb9b47c90eeb93',
'0361aa68deb561a8b47b41165848edcccb98a1b56a5ea922d9d5b30a09bb7282ea',
'0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c',
'029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5',
'02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075',
@@ -76,6 +92,14 @@ class NodesRoutes {
'0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc',
'02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9',
'0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4',
'030bbbd8495561a894e301fe6ba5b22f8941fc661cc0e673e0206158231d8ac130',
'03ee1f08e516ed083475f39c6cae4fa1eec686d004d2f105218269e27d7f2da5a4',
'028c378b998f476ed22d6815c170dd2a3388a43fdf791a7cff70b9997349b8447a',
'036f19f044d19cb1b04f14d91b6e7e5443ce337217a8c14d43861f3e86dd07bd7f',
'03058d61869e8b88436493648b2e3e530627edf5a0b253c285cd565c1477a5c237',
'0279dfedc87b47a941f1797f2c422c03aa3108914ea6b519d76537d60860535a9a',
'0353486b8016761e58ec8aee7305ee58d5dc66b55ef5bd8cbaf49508f66d52d62e',
'03df5db8eccfabcae47ff15553cfdecb2d3f56979f43a0c3578f28d056b5e35104',
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
@@ -88,6 +112,14 @@ class NodesRoutes {
'038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7',
'03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761',
'028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7',
'0326cf9a4ca67a5b9cdffae57293dbd6f7c5113b93010dc6f6fe4af3afde1a1739',
'034867e16f62cebb8c2c2c22b91117c173bbece9c8a1e5bd001374a3699551cd8f',
'038dfb1f1b637a8c27e342ffc6f9feca20e0b47be3244e09ae78df4998e2ae83b9',
'03cb1cea3394d973355c11bc61c2f689f9d3e1c3db60d205f27770f5ad83200f77',
'03535447b592cbdb153189b3e06a455452b1011380cb3e6511a31090c15d8efc9f',
'028e90e9984d262ebfa3c23fb3f335a2ae061a0bdedee03f45f72b438d9e7d2ce3',
'03ee0176289dc4a6111fa5ef22eed5273758c420fbe58cc1d2d76def75dd7e640c',
'0370b2cd9f0eaf436d5c25c93fb39210d8cc06b31f688fc2f54418aabe394aed79',
'02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070',
'02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45',
'038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097',
@@ -104,6 +136,14 @@ class NodesRoutes {
'03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc',
'03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e',
'0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055',
'021b28ecdd782fd909705d6be354db268977b1a2ac5a5275186fc19e08bb8fca93',
'031bec1fbd8eb7fe94d2bda108c9c3cc8c22ecfc1c3a5c11d36f5881b01b4a81a6',
'03879c4f827a3188574d5757e002f574265a966d70aea942169785b31369b067d5',
'0228d4b5a4fd73a03967b76f8b8cb37b9d0b6e7039126a9397bb732c15bed78e9b',
'03f58dbb629f4427f5a1dbc02e6a7ec79345fdf13a0e4163d4f3b7aea2539cf095',
'021cdcb8123aa670cdfc9f43909dbb297363c093883409e9e7fc82e7267f7c72bd',
'02f2aa2c2b7b432a70dc4d0b04afa19d48715ed3b90594d49c1c8744f2e9ebb030',
'03709a02fb3ab4857689a8ea0bd489a6ab6f56f8a397be578bc6d5ad22efbe3756',
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',
@@ -116,6 +156,14 @@ class NodesRoutes {
'02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf',
'03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c',
'0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43',
'03b4dda7878d3b7b71ecd6d4738322c7f9a9c1fb583374d2724f4ccc4947f37570',
'0279a35f05b5acf159429549e56fd426685c4fec191431c58738968bbc77a39f25',
'03cb102d796ddcf08610cd03fae8b7a1df69ff48e9e8a152af315f9edf71762eb8',
'036b89526f4d5ac4c317f4fd23cb9f8e4ad844498bc7950a41114d060101d995d4',
'0313eade145959d7036db009fd5b0bf1947a739c7c3c790b491ec9161b94e6ad1e',
'02b670ca4c4bb2c5ea89c3b691da98a194cfc48fcd5c072df02a20290bddd60610',
'02a9196d5e08598211397a83cf013a5962b84bd61198abfdd204dff987e54f7a0d',
'036d015cd2f486fb38348182980b7e596e6c9733873102ea126fed7b4152be03b8',
'02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06',
'0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e',
'03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7',

View File

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

View File

@@ -27,6 +27,7 @@ class Mempool {
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
private txPerSecondArray: number[] = [];
private txPerSecond: number = 0;
@@ -395,15 +396,15 @@ class Mempool {
}
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
return [];
}
try {
const changed: string[] = [];
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
@@ -510,6 +511,14 @@ class Mempool {
}
}
setAccelerationPositions(positions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] }): void {
this.accelerationPositions = positions;
}
getAccelerationPositions(txid: string): { [pool: number]: { poolId: number, pool: string, block: number, vsize: number } } | undefined {
return this.accelerationPositions[txid];
}
private startTimer() {
const state: any = {
start: Date.now(),

View File

@@ -0,0 +1,515 @@
import { Acceleration } from './acceleration/acceleration';
import { MempoolTransactionExtended } from '../mempool.interfaces';
import logger from '../logger';
const BLOCK_WEIGHT_UNITS = 4_000_000;
const BLOCK_SIGOPS = 80_000;
const MAX_RELATIVE_GRAPH_SIZE = 100;
export interface GraphTx {
txid: string;
vsize: number;
weight: number;
depends: string[];
spentby: string[];
ancestorcount: number;
ancestorsize: number;
fees: { // in sats
base: number;
ancestor: number;
};
ancestors: Map<string, GraphTx>,
ancestorRate: number;
individualRate: number;
score: number;
}
interface TemplateTransaction {
txid: string;
order: number;
weight: number;
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
sigops: number;
fee: number;
feeDelta: number;
ancestors: string[];
cluster: string[];
effectiveFeePerVsize: number;
}
interface MinerTransaction extends TemplateTransaction {
inputs: string[];
feePerVsize: number;
relativesSet: boolean;
ancestorMap: Map<string, MinerTransaction>;
children: Set<MinerTransaction>;
ancestorFee: number;
ancestorVsize: number;
ancestorSigops: number;
score: number;
used: boolean;
modified: boolean;
dependencyRate: number;
}
/**
* Takes a raw transaction, and builds a graph of same-block relatives,
* and returns as a GraphTx
*
* @param tx
*/
export function getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
for (const tx of transactions) {
blockTxs.set(tx.txid, tx);
for (const vin of tx.vin) {
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
}
}
const relatives: Map<string, GraphTx> = new Map();
const stack: string[] = [tx.txid];
// build set of same-block ancestors
while (stack.length > 0) {
const nextTxid = stack.pop();
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
if (!nextTx || relatives.has(nextTx.txid)) {
continue;
}
const mempoolTx = convertToGraphTx(nextTx, spendMap);
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
if (txid) {
stack.push(txid);
}
}
relatives.set(mempoolTx.txid, mempoolTx);
}
return relatives;
}
/**
* Takes a raw transaction and converts it to GraphTx format
* fee and ancestor data is initialized with dummy/null values
*
* @param tx
*/
export function convertToGraphTx(tx: MempoolTransactionExtended, spendMap?: Map<string, MempoolTransactionExtended | string>): GraphTx {
return {
txid: tx.txid,
vsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
weight: tx.weight,
fees: {
base: tx.fee || 0,
ancestor: tx.fee || 0,
},
depends: (tx.vin.map(vin => vin.txid).filter(depend => depend) as string[]),
spentby: spendMap ? (tx.vout.map((vout, index) => { const spend = spendMap.get(`${tx.txid}:${index}`); return (spend?.['txid'] || spend); }).filter(spent => spent) as string[]) : [],
ancestorcount: 1,
ancestorsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
ancestors: new Map<string, GraphTx>(),
ancestorRate: 0,
individualRate: 0,
score: 0,
};
}
/**
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
*/
export function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>, spendMap: Map<string, MempoolTransactionExtended>): Map<string, GraphTx> {
const relatives: Map<string, GraphTx> = new Map();
const stack: GraphTx[] = Array.from(ancestors.values());
while (stack.length > 0) {
if (relatives.size > MAX_RELATIVE_GRAPH_SIZE) {
return relatives;
}
const nextTx = stack.pop();
if (!nextTx) {
continue;
}
relatives.set(nextTx.txid, nextTx);
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
if (relatives.has(relativeTxid)) {
// already processed this tx
continue;
}
let ancestorTx = ancestors.get(relativeTxid);
if (!ancestorTx && relativeTxid in mempool) {
const mempoolTx = mempool[relativeTxid];
ancestorTx = convertToGraphTx(mempoolTx, spendMap);
}
if (ancestorTx) {
stack.push(ancestorTx);
}
}
}
return relatives;
}
/**
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
* for each transaction.
*
* @param tx
* @param all
*/
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
// sanity check for infinite recursion / too many ancestors (should never happen)
if (depth > MAX_RELATIVE_GRAPH_SIZE) {
logger.warn('cpfp dependency calculation failed: setAncestors reached depth of 100, unable to proceed');
return tx.ancestors;
}
// initialize the ancestor map for this tx
tx.ancestors = new Map<string, GraphTx>();
tx.depends.forEach(parentId => {
const parent = all.get(parentId);
if (parent) {
// add the parent
tx.ancestors?.set(parentId, parent);
// check for a cached copy of this parent's ancestors
let ancestors = visited.get(parent.txid);
if (!ancestors) {
// recursively fetch the parent's ancestors
ancestors = setAncestors(parent, all, visited, depth + 1);
}
// and add to this tx's map
ancestors.forEach((ancestor, ancestorId) => {
tx.ancestors?.set(ancestorId, ancestor);
});
}
});
visited.set(tx.txid, tx.ancestors);
return tx.ancestors;
}
/**
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
* by running setAncestors on each leaf, and caching intermediate results.
* then initializes ancestor data for each transaction
*
* @param all
*/
export function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
const visited: Map<string, Map<string, GraphTx>> = new Map();
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
for (const leaf of leaves) {
setAncestors(leaf, mempoolTxs, visited);
}
mempoolTxs.forEach(entry => {
entry.ancestors?.forEach(ancestor => {
entry.ancestorcount++;
entry.ancestorsize += ancestor.vsize;
entry.fees.ancestor += ancestor.fees.base;
});
setAncestorScores(entry);
});
return mempoolTxs;
}
/**
* Remove a cluster of transactions from an in-mempool dependency graph
* and update the survivors' scores and ancestors
*
* @param cluster
* @param ancestors
*/
export function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
// remove
cluster.forEach(tx => {
all.delete(tx.txid);
});
// update survivors
all.forEach(tx => {
cluster.forEach(remove => {
if (tx.ancestors?.has(remove.txid)) {
// remove as dependency
tx.ancestors.delete(remove.txid);
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
// update ancestor sizes and fees
tx.ancestorsize -= remove.vsize;
tx.fees.ancestor -= remove.fees.base;
}
});
// recalculate fee rates
setAncestorScores(tx);
});
}
/**
* Take a mempool transaction, and set the fee rates and ancestor score
*
* @param tx
*/
export function setAncestorScores(tx: GraphTx): void {
tx.individualRate = tx.fees.base / tx.vsize;
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
}
// Sort by descending score
export function mempoolComparator(a: GraphTx, b: GraphTx): number {
return b.score - a.score;
}
/*
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/
export function makeBlockTemplate(candidates: MempoolTransactionExtended[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
const auditPool: Map<string, MinerTransaction> = new Map();
const mempoolArray: MinerTransaction[] = [];
candidates.forEach(tx => {
// initializing everything up front helps V8 optimize property access later
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
const feePerVsize = (tx.fee / adjustedVsize);
auditPool.set(tx.txid, {
txid: tx.txid,
order: txidToOrdering(tx.txid),
fee: tx.fee,
feeDelta: 0,
weight: tx.weight,
adjustedVsize,
feePerVsize: feePerVsize,
effectiveFeePerVsize: feePerVsize,
dependencyRate: feePerVsize,
sigops: tx.sigops || 0,
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
relativesSet: false,
ancestors: [],
cluster: [],
ancestorMap: new Map<string, MinerTransaction>(),
children: new Set<MinerTransaction>(),
ancestorFee: 0,
ancestorVsize: 0,
ancestorSigops: 0,
score: 0,
used: false,
modified: false,
});
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
});
// set accelerated effective fee
for (const acceleration of accelerations) {
const tx = auditPool.get(acceleration.txid);
if (tx) {
tx.feeDelta = acceleration.max_bid;
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
tx.effectiveFeePerVsize = tx.feePerVsize;
tx.dependencyRate = tx.feePerVsize;
}
}
// Build relatives graph & calculate ancestor scores
for (const tx of mempoolArray) {
if (!tx.relativesSet) {
setRelatives(tx, auditPool);
}
}
// Sort by descending ancestor score
mempoolArray.sort(priorityComparator);
// Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score)
const blocks: number[][] = [];
let blockWeight = 0;
let blockSigops = 0;
const transactions: MinerTransaction[] = [];
let modified: MinerTransaction[] = [];
const overflow: MinerTransaction[] = [];
let failures = 0;
while (mempoolArray.length || modified.length) {
// skip invalid transactions
while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
mempoolArray.shift();
}
// Select best next package
let nextTx;
const nextPoolTx = mempoolArray[0];
const nextModifiedTx = modified[0];
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
nextTx = nextPoolTx;
mempoolArray.shift();
} else {
modified.shift();
if (nextModifiedTx) {
nextTx = nextModifiedTx;
}
}
if (nextTx && !nextTx?.used) {
// Check if the package fits into this block
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
const clusterTxids = sortedTxSet.map(tx => tx.txid);
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
const used: MinerTransaction[] = [];
while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop();
if (!ancestor) {
continue;
}
ancestor.used = true;
ancestor.usedBy = nextTx.txid;
// update this tx with effective fee rate & relatives data
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
ancestor.effectiveFeePerVsize = effectiveFeeRate;
}
ancestor.cluster = clusterTxids;
transactions.push(ancestor);
blockWeight += ancestor.weight;
blockSigops += ancestor.sigops;
used.push(ancestor);
}
// remove these as valid package ancestors for any descendants remaining in the mempool
if (used.length) {
used.forEach(tx => {
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
});
}
failures = 0;
} else {
// hold this package in an overflow list while we check for smaller options
overflow.push(nextTx);
failures++;
}
}
// this block is full
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
const queueEmpty = !mempoolArray.length && !modified.length;
if (exceededPackageTries || queueEmpty) {
break;
}
}
for (const tx of transactions) {
tx.ancestors = Object.values(tx.ancestorMap);
}
return transactions;
}
// traverse in-mempool ancestors
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives(
tx: MinerTransaction,
mempool: Map<string, MinerTransaction>,
): void {
for (const parent of tx.inputs) {
const parentTx = mempool.get(parent);
if (parentTx && !tx.ancestorMap?.has(parent)) {
tx.ancestorMap.set(parent, parentTx);
parentTx.children.add(tx);
// visit each node only once
if (!parentTx.relativesSet) {
setRelatives(parentTx, mempool);
}
parentTx.ancestorMap.forEach((ancestor) => {
tx.ancestorMap.set(ancestor.txid, ancestor);
});
}
};
tx.ancestorFee = (tx.fee + tx.feeDelta);
tx.ancestorVsize = tx.adjustedVsize || 0;
tx.ancestorSigops = tx.sigops || 0;
tx.ancestorMap.forEach((ancestor) => {
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
tx.ancestorVsize += ancestor.adjustedVsize;
tx.ancestorSigops += ancestor.sigops;
});
tx.score = tx.ancestorFee / tx.ancestorVsize;
tx.relativesSet = true;
}
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
// avoids recursion to limit call stack depth
function updateDescendants(
rootTx: MinerTransaction,
mempool: Map<string, MinerTransaction>,
modified: MinerTransaction[],
clusterRate: number,
): MinerTransaction[] {
const descendantSet: Set<MinerTransaction> = new Set();
// stack of nodes left to visit
const descendants: MinerTransaction[] = [];
let descendantTx: MinerTransaction | undefined;
rootTx.children.forEach(childTx => {
if (!descendantSet.has(childTx)) {
descendants.push(childTx);
descendantSet.add(childTx);
}
});
while (descendants.length) {
descendantTx = descendants.pop();
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
// remove tx as ancestor
descendantTx.ancestorMap.delete(rootTx.txid);
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
descendantTx.ancestorSigops -= rootTx.sigops;
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
if (!descendantTx.modified) {
descendantTx.modified = true;
modified.push(descendantTx);
}
// add this node's children to the stack
descendantTx.children.forEach(childTx => {
// visit each node only once
if (!descendantSet.has(childTx)) {
descendants.push(childTx);
descendantSet.add(childTx);
}
});
}
}
// return new, resorted modified list
return modified.sort(priorityComparator);
}
// Used to sort an array of MinerTransactions by descending ancestor score
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
if (b.score === a.score) {
// tie-break by txid for stability
return a.order - b.order;
} else {
return b.score - a.score;
}
}
// returns the most significant 4 bytes of the txid as an integer
function txidToOrdering(txid: string): number {
return parseInt(
txid.substring(62, 64) +
txid.substring(60, 62) +
txid.substring(58, 60) +
txid.substring(56, 58),
16
);
}

View File

@@ -9,6 +9,7 @@ import bitcoinClient from '../bitcoin/bitcoin-client';
import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration';
class MiningRoutes {
public initRoutes(app: Application) {
@@ -24,6 +25,7 @@ class MiningRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees', this.$getBlockFeesTimespan)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
@@ -40,6 +42,8 @@ class MiningRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations)
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals)
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations', this.$getActiveAccelerations)
.post(config.MEMPOOL.API_URL_PREFIX + 'acceleration/request/:txid', this.$requestAcceleration)
;
}
@@ -217,6 +221,24 @@ class MiningRoutes {
}
}
private async $getBlockFeesTimespan(req: Request, res: Response) {
try {
if (!parseInt(req.query.from as string, 10) || !parseInt(req.query.to as string, 10)) {
throw new Error('Invalid timestamp range');
}
if (parseInt(req.query.from as string, 10) > parseInt(req.query.to as string, 10)) {
throw new Error('from must be less than to');
}
const blockFees = await mining.$getBlockFeesTimespan(parseInt(req.query.from as string, 10), parseInt(req.query.to as string, 10));
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getHistoricalBlockRewards(req: Request, res: Response) {
try {
const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval);
@@ -426,6 +448,33 @@ class MiningRoutes {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getActiveAccelerations(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.');
return;
}
res.status(200).send(accelerationApi.accelerations || []);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $requestAcceleration(req: Request, res: Response): Promise<void> {
res.setHeader('Pragma', 'no-cache');
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
res.setHeader('expires', -1);
try {
accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send();
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new MiningRoutes();

View File

@@ -45,11 +45,22 @@ class Mining {
*/
public async $getHistoricalBlockFees(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockFees(
this.getTimeRange(interval, 5),
this.getTimeRange(interval),
Common.getSqlInterval(interval)
);
}
/**
* Get timespan block total fees
*/
public async $getBlockFeesTimespan(from: number, to: number): Promise<number> {
return await BlocksRepository.$getHistoricalBlockFees(
this.getTimeRangeFromTimespan(from, to),
null,
{from, to}
);
}
/**
* Get historical block rewards
*/
@@ -646,6 +657,24 @@ class Mining {
}
}
private getTimeRangeFromTimespan(from: number, to: number, scale = 1): number {
const timespan = to - from;
switch (true) {
case timespan > 3600 * 24 * 365 * 4: return 86400 * scale; // 24h
case timespan > 3600 * 24 * 365 * 3: return 43200 * scale; // 12h
case timespan > 3600 * 24 * 365 * 2: return 43200 * scale; // 12h
case timespan > 3600 * 24 * 365: return 28800 * scale; // 8h
case timespan > 3600 * 24 * 30 * 6: return 28800 * scale; // 8h
case timespan > 3600 * 24 * 30 * 3: return 10800 * scale; // 3h
case timespan > 3600 * 24 * 30: return 7200 * scale; // 2h
case timespan > 3600 * 24 * 7: return 1800 * scale; // 30min
case timespan > 3600 * 24 * 3: return 300 * scale; // 5min
case timespan > 3600 * 24: return 1 * scale;
default: return 1 * scale;
}
}
// Finds the oldest block in a consecutive chain back from the tip
// assumes `blocks` is sorted in ascending height order
private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock {

View File

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

View File

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

View File

@@ -1,12 +1,23 @@
import config from '../../config';
import logger from '../../logger';
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
import { BlockExtended } from '../../mempool.interfaces';
import axios from 'axios';
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
export interface Acceleration {
txid: string,
added: number,
effectiveVsize: number,
effectiveFee: number,
feeDelta: number,
pools: number[],
positions?: {
[pool: number]: {
block: number,
vbytes: number,
},
},
};
export interface AccelerationHistory {
@@ -22,25 +33,95 @@ export interface AccelerationHistory {
feeDelta: number,
blockHash: string,
blockHeight: number,
pools: {
pool_unique_id: number,
username: string,
}[],
pools: number[];
};
class AccelerationApi {
public async $fetchAccelerations(): Promise<Acceleration[] | null> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
try {
const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`, { responseType: 'json', timeout: 10000 });
return response.data as Acceleration[];
} catch (e) {
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
return null;
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
private _accelerations: Acceleration[] | null = null;
private lastPoll = 0;
private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
public get accelerations(): Acceleration[] | null {
return this._accelerations;
}
public countMyAccelerationsWithStatus(filter: MyAccelerationStatus): number {
return Object.values(this.myAccelerations).reduce((count, {status}) => { return count + (status === filter ? 1 : 0); }, 0);
}
public accelerationRequested(txid: string): void {
if (this.onDemandPollingEnabled) {
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
}
}
public accelerationConfirmed(): void {
this.forcePoll = true;
}
private async $fetchAccelerations(): Promise<Acceleration[] | null> {
try {
const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 });
return response?.data || [];
} catch (e) {
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
return null;
}
}
public async $updateAccelerations(): Promise<Acceleration[] | null> {
if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations();
if (accelerations) {
this._accelerations = accelerations;
return this._accelerations;
}
} else {
return [];
return this.$updateAccelerationsOnDemand();
}
return null;
}
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
const shouldUpdate = this.forcePoll
|| this.countMyAccelerationsWithStatus('requested') > 0
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
// update accelerations if necessary
if (shouldUpdate) {
const accelerations = await this.$fetchAccelerations();
this.lastPoll = Date.now();
this.forcePoll = false;
if (accelerations) {
const latestAccelerations: Record<string, Acceleration> = {};
// set relevant accelerations to 'accelerating'
for (const acc of accelerations) {
if (this.myAccelerations[acc.txid]) {
latestAccelerations[acc.txid] = acc;
this.myAccelerations[acc.txid] = { status: 'accelerating', added: Date.now(), acceleration: acc };
}
}
// txs that are no longer accelerating are either confirmed or canceled, so mark for expiry
for (const [txid, { status, acceleration }] of Object.entries(this.myAccelerations)) {
if (status === 'accelerating' && !latestAccelerations[txid]) {
this.myAccelerations[txid] = { status: 'done', added: Date.now(), acceleration };
}
}
}
}
// clear expired accelerations (confirmed / failed / not accepted) after 10 minutes
for (const [txid, { status, added }] of Object.entries(this.myAccelerations)) {
if (['requested', 'done'].includes(status) && added < (Date.now() - (1000 * 60 * 10))) {
delete this.myAccelerations[txid];
}
}
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
return this._accelerations;
}
public async $fetchAccelerationHistory(page?: number, status?: string): Promise<AccelerationHistory[] | null> {

View File

@@ -64,7 +64,7 @@ class StatisticsApi {
}
}
public async $create(statistics: Statistic): Promise<number | undefined> {
public async $create(statistics: Statistic, convertToDatetime = false): Promise<number | undefined> {
try {
const query = `INSERT INTO statistics(
added,
@@ -114,7 +114,7 @@ class StatisticsApi {
vsize_1800,
vsize_2000
)
VALUES (${statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
VALUES (${convertToDatetime ? `FROM_UNIXTIME(${statistics.added})` : statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params: (string | number)[] = [
@@ -456,6 +456,59 @@ class StatisticsApi {
};
});
}
public mapOptimizedStatisticToStatistic(statistic: OptimizedStatistic[]): Statistic[] {
return statistic.map((s) => {
return {
added: s.added,
unconfirmed_transactions: s.count,
tx_per_second: 0,
vbytes_per_second: s.vbytes_per_second,
mempool_byte_weight: s.mempool_byte_weight || 0,
total_fee: s.total_fee || 0,
min_fee: s.min_fee,
fee_data: '',
vsize_1: s.vsizes[0],
vsize_2: s.vsizes[1],
vsize_3: s.vsizes[2],
vsize_4: s.vsizes[3],
vsize_5: s.vsizes[4],
vsize_6: s.vsizes[5],
vsize_8: s.vsizes[6],
vsize_10: s.vsizes[7],
vsize_12: s.vsizes[8],
vsize_15: s.vsizes[9],
vsize_20: s.vsizes[10],
vsize_30: s.vsizes[11],
vsize_40: s.vsizes[12],
vsize_50: s.vsizes[13],
vsize_60: s.vsizes[14],
vsize_70: s.vsizes[15],
vsize_80: s.vsizes[16],
vsize_90: s.vsizes[17],
vsize_100: s.vsizes[18],
vsize_125: s.vsizes[19],
vsize_150: s.vsizes[20],
vsize_175: s.vsizes[21],
vsize_200: s.vsizes[22],
vsize_250: s.vsizes[23],
vsize_300: s.vsizes[24],
vsize_350: s.vsizes[25],
vsize_400: s.vsizes[26],
vsize_500: s.vsizes[27],
vsize_600: s.vsizes[28],
vsize_700: s.vsizes[29],
vsize_800: s.vsizes[30],
vsize_900: s.vsizes[31],
vsize_1000: s.vsizes[32],
vsize_1200: s.vsizes[33],
vsize_1400: s.vsizes[34],
vsize_1600: s.vsizes[35],
vsize_1800: s.vsizes[36],
vsize_2000: s.vsizes[37],
}
});
}
}
export default new StatisticsApi();

View File

@@ -103,7 +103,7 @@ class TransactionUtils {
}
const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4);
const transactionExtended: TransactionExtended = Object.assign({
vsize: Math.round(transaction.weight / 4),
vsize: transaction.weight / 4,
feePerVsize: feePerVbytes,
effectiveFeePerVsize: feePerVbytes,
}, transaction);
@@ -123,7 +123,7 @@ class TransactionUtils {
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
order: this.txidToOrdering(transaction.txid),
vsize: Math.round(transaction.weight / 4),
vsize,
adjustedVsize,
sigops,
feePerVsize: feePerVbytes,

View File

@@ -3,6 +3,7 @@ import * as WebSocket from 'ws';
import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolDelta, MempoolDeltaTxids
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -32,7 +33,7 @@ interface AddressTransactions {
removed: MempoolTransactionExtended[],
}
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateCpfp } from './cpfp';
import { calculateMempoolTxCpfp } from './cpfp';
// valid 'want' subscriptions
const wantable = [
@@ -44,7 +45,7 @@ const wantable = [
];
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
private webSocketServers: WebSocket.Server[] = [];
private extraInitProperties = {};
private numClients = 0;
@@ -54,11 +55,12 @@ class WebsocketHandler {
private socketData: { [key: string]: string } = {};
private serializedInitData: string = '{}';
private lastRbfSummary: ReplacementInfo[] | null = null;
private mempoolSequence: number = 0;
constructor() { }
setWebsocketServer(wss: WebSocket.Server) {
this.wss = wss;
addWebsocketServer(wss: WebSocket.Server) {
this.webSocketServers.push(wss);
}
setExtraInitData(property: string, value: any) {
@@ -102,11 +104,13 @@ class WebsocketHandler {
}
setupConnectionHandling() {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.wss.on('connection', (client: WebSocket, req) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.on('connection', (client: WebSocket, req) => {
this.numConnected++;
client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
client.on('error', (e) => {
@@ -202,7 +206,8 @@ class WebsocketHandler {
}
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position
position,
accelerationPositions: memPool.getAccelerationPositions(tx.txid),
});
}
} else {
@@ -315,6 +320,7 @@ class WebsocketHandler {
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
response['projected-block-transactions'] = JSON.stringify({
index: index,
sequence: this.mempoolSequence,
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
});
} else {
@@ -342,6 +348,17 @@ class WebsocketHandler {
}
}
if (parsedMessage && parsedMessage['track-accelerations'] != null) {
if (parsedMessage['track-accelerations']) {
client['track-accelerations'] = true;
response['accelerations'] = JSON.stringify({
accelerations: Object.values(memPool.getAccelerations()),
});
} else {
client['track-accelerations'] = false;
}
}
if (parsedMessage.action === 'init') {
if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) {
this.updateSocketData();
@@ -360,6 +377,18 @@ class WebsocketHandler {
client['track-donation'] = parsedMessage['track-donation'];
}
if (parsedMessage['track-mempool-txids'] === true) {
client['track-mempool-txids'] = true;
} else if (parsedMessage['track-mempool-txids'] === false) {
delete client['track-mempool-txids'];
}
if (parsedMessage['track-mempool'] === true) {
client['track-mempool'] = true;
} else if (parsedMessage['track-mempool'] === false) {
delete client['track-mempool'];
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
@@ -369,14 +398,17 @@ class WebsocketHandler {
}
});
});
}
}
handleNewDonation(id: string) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -384,43 +416,50 @@ class WebsocketHandler {
client.send(JSON.stringify({ donationConfirmed: true }));
}
});
}
}
handleLoadingChanged(indicators: ILoadingIndicators) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.updateSocketDataFields({ 'loadingIndicators': indicators });
const response = JSON.stringify({ loadingIndicators: indicators });
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
}
handleNewConversionRates(conversionRates: ApiPrice) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.updateSocketDataFields({ 'conversions': conversionRates });
const response = JSON.stringify({ conversions: conversionRates });
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
}
handleNewStatistic(stats: OptimizedStatistic) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.printLogs();
@@ -429,7 +468,9 @@ class WebsocketHandler {
'live-2h-chart': stats
});
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -440,11 +481,12 @@ class WebsocketHandler {
client.send(response);
});
}
}
handleReorg(): void {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
const da = difficultyAdjustment.getDifficultyAdjustment();
@@ -455,7 +497,9 @@ class WebsocketHandler {
'da': da?.previousTime ? da : undefined,
});
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -473,13 +517,14 @@ class WebsocketHandler {
client.send(this.serializeResponse(response));
}
});
}
}
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.printLogs();
@@ -493,9 +538,9 @@ class WebsocketHandler {
}
if (config.MEMPOOL.RUST_GBT) {
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, config.MEMPOOL_SERVICES.ACCELERATIONS);
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, true);
} else {
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, true);
}
const mBlocks = mempoolBlocks.getMempoolBlocks();
@@ -504,6 +549,7 @@ class WebsocketHandler {
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements;
@@ -525,6 +571,33 @@ class WebsocketHandler {
const latestTransactions = memPool.getLatestTransactions();
if (memPool.isInSync()) {
this.mempoolSequence++;
}
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid]) {
replacedTransactions.push({ replaced: replaced.txid, by: tx });
}
}
}
const mempoolDeltaTxids: MempoolDeltaTxids = {
sequence: this.mempoolSequence,
added: newTransactions.map(tx => tx.txid),
removed: deletedTransactions.map(tx => tx.txid),
mined: [],
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
};
const mempoolDelta: MempoolDelta = {
sequence: this.mempoolSequence,
added: newTransactions,
removed: deletedTransactions.map(tx => tx.txid),
mined: [],
replaced: replacedTransactions,
};
// update init data
const socketDataFields = {
'mempoolInfo': mempoolInfo,
@@ -552,7 +625,9 @@ class WebsocketHandler {
// pre-compute new tracked outspends
const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {};
const trackedTxs = new Set<string>();
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client['track-tx']) {
trackedTxs.add(client['track-tx']);
}
@@ -562,6 +637,7 @@ class WebsocketHandler {
}
}
});
}
if (trackedTxs.size > 0) {
for (const tx of newTransactions) {
for (let i = 0; i < tx.vin.length; i++) {
@@ -581,7 +657,15 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
this.wss.clients.forEach(async (client) => {
// pre-compute acceleration delta
const accelerationUpdate = {
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]),
};
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach(async (client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -737,10 +821,14 @@ class WebsocketHandler {
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
};
if (!mempoolTx.cpfpChecked) {
calculateCpfp(mempoolTx, newMempool);
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
calculateMempoolTxCpfp(mempoolTx, newMempool);
}
if (mempoolTx.cpfpDirty) {
positionData['cpfp'] = {
@@ -750,7 +838,7 @@ class WebsocketHandler {
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
sigops: mempoolTx.sigops,
adjustedVsize: mempoolTx.adjustedVsize,
acceleration: mempoolTx.acceleration
acceleration: mempoolTx.acceleration,
};
}
response['txPosition'] = JSON.stringify(positionData);
@@ -775,9 +863,12 @@ class WebsocketHandler {
txInfo.position = {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
if (!mempoolTx.cpfpChecked) {
calculateCpfp(mempoolTx, newMempool);
calculateMempoolTxCpfp(mempoolTx, newMempool);
}
if (mempoolTx.cpfpDirty) {
txInfo.cpfp = {
@@ -802,6 +893,7 @@ class WebsocketHandler {
if (mBlockDeltas[index]) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
});
}
@@ -817,17 +909,32 @@ class WebsocketHandler {
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
}
if (client['track-mempool-txids']) {
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
}
if (client['track-mempool']) {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
}
if (client['track-accelerations'] && (accelerationUpdate.added.length || accelerationUpdate.removed.length)) {
response['accelerations'] = getCachedResponse('accelerations', accelerationUpdate);
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
});
}
}
async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise<void> {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
const blockTransactions = structuredClone(transactions);
this.printLogs();
await statistics.runStatistics();
@@ -837,7 +944,7 @@ class WebsocketHandler {
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
const accelerations = Object.values(mempool.getAccelerations());
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions);
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions);
@@ -846,22 +953,18 @@ class WebsocketHandler {
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
let projectedBlocks;
const auditMempool = _memPool;
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
const isAccelerated = accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
if (config.MEMPOOL.RUST_GBT) {
const added = memPool.limitGBT ? (candidates?.added || []) : [];
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
} else {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
}
if (config.MEMPOOL.RUST_GBT) {
const added = memPool.limitGBT ? (candidates?.added || []) : [];
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
} else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
}
if (Common.indexingEnabled()) {
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@@ -883,9 +986,11 @@ class WebsocketHandler {
});
BlocksAuditsRepository.$saveAudit({
version: 1,
time: block.timestamp,
height: block.height,
hash: block.id,
unseenTxs: unseen,
addedTxs: added,
prioritizedTxs: prioritized,
missingTxs: censored,
@@ -937,7 +1042,7 @@ class WebsocketHandler {
const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions;
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true);
} else {
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, true);
}
const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
@@ -961,6 +1066,31 @@ class WebsocketHandler {
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
if (memPool.isInSync()) {
this.mempoolSequence++;
}
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const txid of Object.keys(rbfTransactions)) {
for (const replaced of rbfTransactions[txid].replaced) {
replacedTransactions.push({ replaced: replaced.txid, by: rbfTransactions[txid].replacedBy });
}
}
const mempoolDeltaTxids: MempoolDeltaTxids = {
sequence: this.mempoolSequence,
added: [],
removed: [],
mined: transactions.map(tx => tx.txid),
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
};
const mempoolDelta: MempoolDelta = {
sequence: this.mempoolSequence,
added: [],
removed: [],
mined: transactions.map(tx => tx.txid),
replaced: replacedTransactions,
};
const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string {
if (!responseCache[key]) {
@@ -969,7 +1099,9 @@ class WebsocketHandler {
return responseCache[key];
}
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -1010,7 +1142,11 @@ class WebsocketHandler {
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
});
}
}
@@ -1029,6 +1165,9 @@ class WebsocketHandler {
...mempoolTx.position,
},
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
}
}
@@ -1135,21 +1274,32 @@ class WebsocketHandler {
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, {
index: index,
sequence: this.mempoolSequence,
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
});
} else {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
});
}
}
}
if (client['track-mempool-txids']) {
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
}
if (client['track-mempool']) {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
});
}
await statistics.runStatistics();
}
@@ -1158,7 +1308,7 @@ class WebsocketHandler {
// and zips it together into a valid JSON object
private serializeResponse(response): string {
return '{'
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
+ Object.keys(response).filter(key => response[key] != null).map(key => `"${key}": ${response[key]}`).join(', ')
+ '}';
}
@@ -1231,13 +1381,15 @@ class WebsocketHandler {
}
private printLogs(): void {
if (this.wss) {
if (this.webSocketServers.length) {
let numTxSubs = 0;
let numTxsSubs = 0;
let numProjectedSubs = 0;
let numRbfSubs = 0;
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client['track-tx']) {
numTxSubs++;
}
@@ -1251,8 +1403,12 @@ class WebsocketHandler {
numRbfSubs++;
}
})
}
const count = this.wss?.clients?.size || 0;
let count = 0;
for (const server of this.webSocketServers) {
count += server.clients?.size || 0;
}
const diff = count - this.numClients;
this.numClients = count;
logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`);

View File

@@ -9,6 +9,7 @@ interface IConfig {
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
BACKEND: 'esplora' | 'electrum' | 'none';
HTTP_PORT: number;
UNIX_SOCKET_PATH: string;
SPAWN_CLUSTER_PROCS: number;
API_URL_PREFIX: string;
POLL_RATE_MS: number;
@@ -28,7 +29,7 @@ interface IConfig {
EXTERNAL_RETRY_INTERVAL: number;
USER_AGENT: string;
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
AUTOMATIC_BLOCK_REINDEXING: boolean;
AUTOMATIC_POOLS_UPDATE: boolean;
POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string,
AUDIT: boolean;
@@ -50,6 +51,7 @@ interface IConfig {
REQUEST_TIMEOUT: number;
FALLBACK_TIMEOUT: number;
FALLBACK: string[];
MAX_BEHIND_TIP: number;
};
LIGHTNING: {
ENABLED: boolean;
@@ -140,6 +142,8 @@ interface IConfig {
ENABLED: boolean;
AUDIT: boolean;
AUDIT_START_HEIGHT: number;
STATISTICS: boolean;
STATISTICS_START_TIME: number | string;
SERVERS: string[];
},
MEMPOOL_SERVICES: {
@@ -153,6 +157,7 @@ interface IConfig {
},
FIAT_PRICE: {
ENABLED: boolean;
PAID: boolean;
API_KEY: string;
},
}
@@ -164,6 +169,7 @@ const defaults: IConfig = {
'NETWORK': 'mainnet',
'BACKEND': 'none',
'HTTP_PORT': 8999,
'UNIX_SOCKET_PATH': '',
'SPAWN_CLUSTER_PROCS': 0,
'API_URL_PREFIX': '/api/v1/',
'POLL_RATE_MS': 2000,
@@ -183,11 +189,11 @@ const defaults: IConfig = {
'EXTERNAL_RETRY_INTERVAL': 0,
'USER_AGENT': 'mempool',
'STDOUT_LOG_MIN_PRIORITY': 'debug',
'AUTOMATIC_BLOCK_REINDEXING': false,
'AUTOMATIC_POOLS_UPDATE': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
'AUDIT': false,
'RUST_GBT': false,
'RUST_GBT': true,
'LIMIT_GBT': false,
'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0,
@@ -205,6 +211,7 @@ const defaults: IConfig = {
'REQUEST_TIMEOUT': 10000,
'FALLBACK_TIMEOUT': 5000,
'FALLBACK': [],
'MAX_BEHIND_TIP': 2,
},
'ELECTRUM': {
'HOST': '127.0.0.1',
@@ -295,6 +302,8 @@ const defaults: IConfig = {
'ENABLED': false,
'AUDIT': false,
'AUDIT_START_HEIGHT': 774000,
'STATISTICS': false,
'STATISTICS_START_TIME': 1481932800,
'SERVERS': [],
},
'MEMPOOL_SERVICES': {
@@ -308,6 +317,7 @@ const defaults: IConfig = {
},
'FIAT_PRICE': {
'ENABLED': true,
'PAID': false,
'API_KEY': '',
},
};

View File

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

View File

@@ -43,10 +43,15 @@ import redisCache from './api/redis-cache';
import accelerationApi from './api/services/acceleration';
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks';
class Server {
private wss: WebSocket.Server | undefined;
private wssUnixSocket: WebSocket.Server | undefined;
private server: http.Server | undefined;
private serverUnixSocket: http.Server | undefined;
private app: Application;
private currentBackendRetryInterval = 1;
private backendRetryCount = 0;
@@ -127,6 +132,7 @@ class Server {
})
.use(express.urlencoded({ extended: true }))
.use(express.text({ type: ['text/plain', 'application/base64'] }))
.use(express.json())
;
if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) {
@@ -135,11 +141,16 @@ class Server {
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({ server: this.server });
if (config.MEMPOOL.UNIX_SOCKET_PATH) {
this.serverUnixSocket = http.createServer(this.app);
this.wssUnixSocket = new WebSocket.Server({ server: this.serverUnixSocket });
}
this.setUpWebsocketHandling();
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$();
await mempoolBlocks.updatePools$();
if (config.MEMPOOL.ENABLED) {
if (config.MEMPOOL.CACHE_ENABLED) {
await diskCache.$loadMempoolCache();
@@ -190,6 +201,16 @@ class Server {
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
}
});
if (this.serverUnixSocket) {
this.serverUnixSocket.listen(config.MEMPOOL.UNIX_SOCKET_PATH, () => {
if (worker) {
logger.info(`Mempool Server worker #${process.pid} started`);
} else {
logger.notice(`Mempool Server is listening on ${config.MEMPOOL.UNIX_SOCKET_PATH}`);
}
});
}
}
async runMainUpdateLoop(): Promise<void> {
@@ -208,7 +229,7 @@ class Server {
const newMempool = await bitcoinApi.$getRawMempool();
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
const newAccelerations = await accelerationApi.$fetchAccelerations();
const newAccelerations = await accelerationApi.$updateAccelerations();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) {
@@ -263,8 +284,12 @@ class Server {
setUpWebsocketHandling(): void {
if (this.wss) {
websocketHandler.setWebsocketServer(this.wss);
websocketHandler.addWebsocketServer(this.wss);
}
if (this.wssUnixSocket) {
websocketHandler.addWebsocketServer(this.wssUnixSocket);
}
if (Common.isLiquid() && config.DATABASE.ENABLED) {
blocks.setNewBlockCallback(async () => {
try {
@@ -305,6 +330,12 @@ class Server {
nodesRoutes.initRoutes(this.app);
channelsRoutes.initRoutes(this.app);
}
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app);
}
if (!config.MEMPOOL.OFFICIAL) {
aboutRoutes.initRoutes(this.app);
}
}
healthCheck(): void {
@@ -332,6 +363,12 @@ class Server {
if (config.DATABASE.ENABLED) {
DB.releasePidLock();
}
this.server?.close();
this.serverUnixSocket?.close();
this.wss?.close();
if (this.wssUnixSocket) {
this.wssUnixSocket.close();
}
process.exit(code);
}

View File

@@ -8,7 +8,9 @@ import priceUpdater from './tasks/price-updater';
import PricesRepository from './repositories/PricesRepository';
import config from './config';
import auditReplicator from './replication/AuditReplication';
import statisticsReplicator from './replication/StatisticsReplication';
import AccelerationRepository from './repositories/AccelerationRepository';
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
export interface CoreIndex {
name: string;
@@ -181,6 +183,7 @@ class Indexer {
}
this.runSingleTask('blocksPrices');
await blocks.$indexCoinbaseAddresses();
await mining.$indexDifficultyAdjustments();
await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory();
@@ -188,7 +191,9 @@ class Indexer {
await blocks.$generateCPFPDatabase();
await blocks.$generateAuditStats();
await auditReplicator.$sync();
await statisticsReplicator.$sync();
await AccelerationRepository.$indexPastAccelerations();
await BlocksAuditsRepository.$migrateAuditsV0toV1();
// do not wait for classify blocks to finish
blocks.$classifyBlocks();
} catch (e) {

View File

@@ -29,9 +29,11 @@ export interface PoolStats extends PoolInfo {
}
export interface BlockAudit {
version: number,
time: number,
height: number,
hash: string,
unseenTxs: string[],
missingTxs: string[],
freshTxs: string[],
sigopTxs: string[],
@@ -42,6 +44,19 @@ export interface BlockAudit {
matchRate: number,
expectedFees?: number,
expectedWeight?: number,
template?: any[];
}
export interface TransactionAudit {
seen?: boolean;
expected?: boolean;
added?: boolean;
prioritized?: boolean;
delayed?: number;
accelerated?: boolean;
conflict?: boolean;
coinbase?: boolean;
firstSeen?: number;
}
export interface AuditScore {
@@ -71,6 +86,22 @@ export interface MempoolBlockDelta {
changed: MempoolDeltaChange[];
}
export interface MempoolDeltaTxids {
sequence: number,
added: string[];
removed: string[];
mined: string[];
replaced: { replaced: string, by: string }[];
}
export interface MempoolDelta {
sequence: number,
added: MempoolTransactionExtended[];
removed: string[];
mined: string[];
replaced: { replaced: string, by: TransactionExtended }[];
}
interface VinStrippedToScriptsig {
scriptsig: string;
}
@@ -95,6 +126,9 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
vsize: number,
};
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
feeDelta?: number;
replacement?: boolean;
uid?: number;
flags?: number;
@@ -192,6 +226,7 @@ export interface CpfpInfo {
sigops?: number;
adjustedVsize?: number,
acceleration?: boolean,
fee?: number;
}
export interface TransactionStripped {
@@ -270,6 +305,7 @@ export interface BlockExtension {
coinbaseRaw: string;
orphans: OrphanedBlock[] | null;
coinbaseAddress: string | null;
coinbaseAddresses: string[] | null;
coinbaseSignature: string | null;
coinbaseSignatureAscii: string | null;
virtualSize: number;
@@ -349,8 +385,9 @@ export interface CpfpCluster {
}
export interface CpfpSummary {
transactions: TransactionExtended[];
transactions: MempoolTransactionExtended[];
clusters: CpfpCluster[];
version: number;
}
export interface Statistic {
@@ -406,6 +443,7 @@ export interface Statistic {
export interface OptimizedStatistic {
added: string;
count: number;
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
@@ -415,7 +453,7 @@ export interface OptimizedStatistic {
export interface TxTrackingInfo {
replacedBy?: string,
position?: { block: number, vsize: number, accelerated?: boolean },
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
cpfp?: {
ancestors?: Ancestor[],
bestDescendant?: Ancestor | null,
@@ -426,6 +464,9 @@ export interface TxTrackingInfo {
},
utxoSpent?: { [vout: number]: { vin: number, txid: string } },
accelerated?: boolean,
acceleratedBy?: number[],
acceleratedAt?: number,
feeDelta?: number,
confirmed?: boolean
}

View File

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

View File

@@ -0,0 +1,237 @@
import DB from '../database';
import logger from '../logger';
import { $sync } from './replicator';
import config from '../config';
import { Common } from '../api/common';
import statistics from '../api/statistics/statistics-api';
interface MissingStatistics {
'24h': Set<number>;
'1w': Set<number>;
'1m': Set<number>;
'3m': Set<number>;
'6m': Set<number>;
'2y': Set<number>;
'all': Set<number>;
}
const steps = {
'24h': 60,
'1w': 300,
'1m': 1800,
'3m': 7200,
'6m': 10800,
'2y': 28800,
'all': 43200,
};
/**
* Syncs missing statistics data from trusted servers
*/
class StatisticsReplication {
inProgress: boolean = false;
public async $sync(): Promise<void> {
if (!config.REPLICATION.ENABLED || !config.REPLICATION.STATISTICS || !config.STATISTICS.ENABLED) {
// replication not enabled, or statistics not enabled
return;
}
if (this.inProgress) {
logger.info(`StatisticsReplication sync already in progress`, 'Replication');
return;
}
this.inProgress = true;
const missingStatistics = await this.$getMissingStatistics();
const missingIntervals = Object.keys(missingStatistics).filter(key => missingStatistics[key].size > 0);
const totalMissing = missingIntervals.reduce((total, key) => total + missingStatistics[key].size, 0);
if (totalMissing === 0) {
this.inProgress = false;
logger.info(`Statistics table is complete, no replication needed`, 'Replication');
return;
}
for (const interval of missingIntervals) {
logger.debug(`Missing ${missingStatistics[interval].size} statistics rows in '${interval}' timespan`, 'Replication');
}
logger.debug(`Fetching ${missingIntervals.join(', ')} statistics endpoints from trusted servers to fill ${totalMissing} rows missing in statistics`, 'Replication');
let totalSynced = 0;
let totalMissed = 0;
for (const interval of missingIntervals) {
const results = await this.$syncStatistics(interval, missingStatistics[interval]);
totalSynced += results.synced;
totalMissed += results.missed;
logger.info(`Found ${totalSynced} / ${totalSynced + totalMissed} of ${totalMissing} missing statistics rows`, 'Replication');
await Common.sleep$(3000);
}
logger.debug(`Synced ${totalSynced} statistics rows, ${totalMissed} still missing`, 'Replication');
this.inProgress = false;
}
private async $syncStatistics(interval: string, missingTimes: Set<number>): Promise<any> {
let success = false;
let synced = 0;
let missed = new Set(missingTimes);
const syncResult = await $sync(`/api/v1/statistics/${interval}`);
if (syncResult && syncResult.data?.length) {
success = true;
logger.info(`Fetched /api/v1/statistics/${interval} from ${syncResult.server}`);
for (const stat of syncResult.data) {
const time = this.roundToNearestStep(stat.added, steps[interval]);
if (missingTimes.has(time)) {
try {
await statistics.$create(statistics.mapOptimizedStatisticToStatistic([stat])[0], true);
if (missed.delete(time)) {
synced++;
}
} catch (e: any) {
logger.err(`Failed to insert statistics row at ${stat.added} (${interval}) from ${syncResult.server}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
}
} else {
logger.warn(`An error occured when trying to fetch /api/v1/statistics/${interval}`);
}
return { success, synced, missed: missed.size };
}
private async $getMissingStatistics(): Promise<MissingStatistics> {
try {
const now = Math.floor(Date.now() / 1000);
const day = 60 * 60 * 24;
const startTime = this.getStartTimeFromConfig();
const missingStatistics: MissingStatistics = {
'24h': new Set<number>(),
'1w': new Set<number>(),
'1m': new Set<number>(),
'3m': new Set<number>(),
'6m': new Set<number>(),
'2y': new Set<number>(),
'all': new Set<number>()
};
const intervals = [ // [start, end, label ]
[now - day + 600, now - 60, '24h'] , // from 24 hours ago to now = 1 minute granularity
startTime < now - day ? [now - day * 7, now - day, '1w' ] : null, // from 1 week ago to 24 hours ago = 5 minutes granularity
startTime < now - day * 7 ? [now - day * 30, now - day * 7, '1m' ] : null, // from 1 month ago to 1 week ago = 30 minutes granularity
startTime < now - day * 30 ? [now - day * 90, now - day * 30, '3m' ] : null, // from 3 months ago to 1 month ago = 2 hours granularity
startTime < now - day * 90 ? [now - day * 180, now - day * 90, '6m' ] : null, // from 6 months ago to 3 months ago = 3 hours granularity
startTime < now - day * 180 ? [now - day * 365 * 2, now - day * 180, '2y' ] : null, // from 2 years ago to 6 months ago = 8 hours granularity
startTime < now - day * 365 * 2 ? [startTime, now - day * 365 * 2, 'all'] : null, // from start of statistics to 2 years ago = 12 hours granularity
];
for (const interval of intervals) {
if (!interval) {
continue;
}
missingStatistics[interval[2] as string] = await this.$getMissingStatisticsInterval(interval, startTime);
}
return missingStatistics;
} catch (e: any) {
logger.err(`Cannot fetch missing statistics times from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
private async $getMissingStatisticsInterval(interval: any, startTime: number): Promise<Set<number>> {
try {
const start = interval[0];
const end = interval[1];
const step = steps[interval[2]];
const [rows]: any[] = await DB.query(`
SELECT UNIX_TIMESTAMP(added) as added
FROM statistics
WHERE added >= FROM_UNIXTIME(?) AND added <= FROM_UNIXTIME(?)
GROUP BY UNIX_TIMESTAMP(added) DIV ${step} ORDER BY statistics.added DESC
`, [start, end]);
const startingTime = Math.max(startTime, start) - Math.max(startTime, start) % step;
const timeSteps: number[] = [];
for (let time = startingTime; time < end; time += step) {
timeSteps.push(time);
}
if (timeSteps.length === 0) {
return new Set<number>();
}
const roundedTimesAlreadyHere: number[] = Array.from(new Set(rows.map(row => this.roundToNearestStep(row.added, step))));
const missingTimes = timeSteps.filter(time => !roundedTimesAlreadyHere.includes(time)).filter((time, i, arr) => {
// Remove outsiders
if (i === 0) {
return arr[i + 1] === time + step
} else if (i === arr.length - 1) {
return arr[i - 1] === time - step;
}
return (arr[i + 1] === time + step) && (arr[i - 1] === time - step)
});
// Don't bother fetching if very few rows are missing
if (missingTimes.length < timeSteps.length * 0.01) {
return new Set();
}
return new Set(missingTimes);
} catch (e: any) {
logger.err(`Cannot fetch missing statistics times from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
private roundToNearestStep(time: number, step: number): number {
const remainder = time % step;
if (remainder < step / 2) {
return time - remainder;
} else {
return time + (step - remainder);
}
}
private getStartTimeFromConfig(): number {
const now = Math.floor(Date.now() / 1000);
const day = 60 * 60 * 24;
let startTime: number;
if (typeof(config.REPLICATION.STATISTICS_START_TIME) === 'string' && ['24h', '1w', '1m', '3m', '6m', '2y', 'all'].includes(config.REPLICATION.STATISTICS_START_TIME)) {
if (config.REPLICATION.STATISTICS_START_TIME === 'all') {
startTime = 1481932800;
} else if (config.REPLICATION.STATISTICS_START_TIME === '2y') {
startTime = now - day * 365 * 2;
} else if (config.REPLICATION.STATISTICS_START_TIME === '6m') {
startTime = now - day * 180;
} else if (config.REPLICATION.STATISTICS_START_TIME === '3m') {
startTime = now - day * 90;
} else if (config.REPLICATION.STATISTICS_START_TIME === '1m') {
startTime = now - day * 30;
} else if (config.REPLICATION.STATISTICS_START_TIME === '1w') {
startTime = now - day * 7;
} else {
startTime = now - day;
}
} else {
startTime = Math.max(config.REPLICATION.STATISTICS_START_TIME as number || 1481932800, 1481932800);
}
return startTime;
}
}
export default new StatisticsReplication();

View File

@@ -1,4 +1,4 @@
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration';
import { AccelerationInfo } from '../api/acceleration/acceleration';
import { RowDataPacket } from 'mysql2';
import DB from '../database';
import logger from '../logger';
@@ -6,15 +6,17 @@ import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
import { Common } from '../api/common';
import config from '../config';
import blocks from '../api/blocks';
import accelerationApi, { Acceleration } from '../api/services/acceleration';
import accelerationCosts from '../api/acceleration';
import accelerationApi, { Acceleration, AccelerationHistory } from '../api/services/acceleration';
import accelerationCosts from '../api/acceleration/acceleration';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import transactionUtils from '../api/transaction-utils';
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
import { makeBlockTemplate } from '../api/mini-miner';
export interface PublicAcceleration {
txid: string,
height: number,
added: number,
pool: {
id: number,
slug: string,
@@ -29,15 +31,20 @@ export interface PublicAcceleration {
class AccelerationRepository {
private bidBoostV2Activated = 831580;
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise<void> {
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number, accelerationData: Acceleration[]): Promise<void> {
const accelerationMap: { [txid: string]: Acceleration } = {};
for (const acc of accelerationData) {
accelerationMap[acc.txid] = acc;
}
try {
await DB.query(`
INSERT INTO accelerations(txid, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
INSERT INTO accelerations(txid, requested, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
VALUE (?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
height = ?
`, [
acceleration.txSummary.txid,
accelerationMap[acceleration.txSummary.txid].added,
block.timestamp,
block.height,
pool_id,
@@ -64,7 +71,7 @@ class AccelerationRepository {
}
let query = `
SELECT * FROM accelerations
SELECT *, UNIX_TIMESTAMP(requested) as requested_timestamp, UNIX_TIMESTAMP(added) as block_timestamp FROM accelerations
JOIN pools on pools.unique_id = accelerations.pool
`;
let params: any[] = [];
@@ -99,6 +106,7 @@ class AccelerationRepository {
return rows.map(row => ({
txid: row.txid,
height: row.height,
added: row.requested_timestamp || row.block_timestamp,
pool: {
id: row.id,
slug: row.slug,
@@ -184,6 +192,7 @@ class AccelerationRepository {
}
}
// modifies block transactions
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
for (const tx of transactions) {
@@ -202,9 +211,18 @@ class AccelerationRepository {
const tx = blockTxs[acc.txid];
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations);
}
}
let anyConfirmed = false;
for (const acc of accelerations) {
if (blockTxs[acc.txid]) {
anyConfirmed = true;
}
}
if (anyConfirmed) {
accelerationApi.accelerationConfirmed();
}
const lastSyncedHeight = await this.$getLastSyncedHeight();
// if we've missed any blocks, let the indexer catch up from the last synced height on the next run
if (block.height === lastSyncedHeight + 1) {
@@ -230,13 +248,15 @@ class AccelerationRepository {
logger.debug(`Fetching accelerations between block ${lastSyncedHeight} and ${currentHeight}`);
// Fetch accelerations from mempool.space since the last synced block;
const accelerationsByBlock = {};
const accelerationsByBlock: {[height: number]: AccelerationHistory[]} = {};
const blockHashes = {};
let done = false;
let page = 1;
let count = 0;
try {
while (!done) {
// don't DDoS the services backend
Common.sleep$(500 + (Math.random() * 1000));
const accelerations = await accelerationApi.$fetchAccelerationHistory(page);
page++;
if (!accelerations?.length) {
@@ -297,12 +317,16 @@ class AccelerationRepository {
const feeStats = Common.calcEffectiveFeeStatistics(template);
boostRate = feeStats.medianFee;
}
const accelerationSummaries = accelerations.map(acc => ({
...acc,
pools: acc.pools,
}))
for (const acc of accelerations) {
if (blockTxs[acc.txid]) {
if (blockTxs[acc.txid] && acc.pools.includes(block.extras.pool.id)) {
const tx = blockTxs[acc.txid];
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, accelerationSummaries);
}
}
await this.$setLastSyncedHeight(height);
@@ -317,6 +341,26 @@ class AccelerationRepository {
logger.debug(`Indexing accelerations completed`);
}
/**
* Delete accelerations from the database above blockHeight
*/
public async $deleteAccelerationsFrom(blockHeight: number): Promise<void> {
logger.info(`Delete newer accelerations from height ${blockHeight} from the database`);
try {
const currentSyncedHeight = await this.$getLastSyncedHeight();
if (currentSyncedHeight >= blockHeight) {
await DB.query(`
UPDATE state
SET number = ?
WHERE name = 'last_acceleration_block'
`, [blockHeight - 1]);
}
await DB.query(`DELETE FROM accelerations where height >= ${blockHeight}`);
} catch (e) {
logger.err('Cannot delete indexed accelerations. Reason: ' + (e instanceof Error ? e.message : e));
}
}
}
export default new AccelerationRepository();

View File

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

View File

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

View File

@@ -114,6 +114,43 @@ class BlocksSummariesRepository {
return [];
}
public async $getSummariesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> {
try {
const [rows]: any[] = await DB.query(`
SELECT
height,
id,
version
FROM blocks_summaries
WHERE version < ?
ORDER BY height DESC;`, [version]);
return rows;
} catch (e) {
logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e));
}
return [];
}
public async $getTemplatesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> {
try {
const [rows]: any[] = await DB.query(`
SELECT
blocks_summaries.height as height,
blocks_templates.id as id,
blocks_templates.version as version
FROM blocks_templates
JOIN blocks_summaries ON blocks_templates.id = blocks_summaries.id
WHERE blocks_templates.version < ?
ORDER BY height DESC;`, [version]);
return rows;
} catch (e) {
logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e));
}
return [];
}
/**
* Get the fee percentiles if the block has already been indexed, [] otherwise
*

View File

@@ -91,6 +91,26 @@ class CpfpRepository {
return;
}
public async $getClustersAt(height: number): Promise<CpfpCluster[]> {
const [clusterRows]: any = await DB.query(
`
SELECT *
FROM compact_cpfp_clusters
WHERE height = ?
`,
[height]
);
return clusterRows.map(cluster => {
if (cluster?.txs) {
cluster.effectiveFeePerVsize = cluster.fee_rate;
cluster.txs = this.unpack(cluster.txs);
return cluster;
} else {
return null;
}
}).filter(cluster => cluster !== null);
}
public async $deleteClustersFrom(height: number): Promise<void> {
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
try {
@@ -122,6 +142,37 @@ class CpfpRepository {
}
}
public async $deleteClustersAt(height: number): Promise<void> {
logger.info(`Delete cpfp clusters at height ${height} from the database`);
try {
const [rows] = await DB.query(
`
SELECT txs, height, root from compact_cpfp_clusters
WHERE height = ?
`,
[height]
) as RowDataPacket[][];
if (rows?.length) {
for (const clusterToDelete of rows) {
const txs = this.unpack(clusterToDelete?.txs);
for (const tx of txs) {
await transactionRepository.$removeTransaction(tx.txid);
}
}
}
await DB.query(
`
DELETE from compact_cpfp_clusters
WHERE height = ?
`,
[height]
);
} catch (e: any) {
logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
// insert a dummy row to mark that we've indexed as far as this block
public async $insertProgressMarker(height: number): Promise<void> {
try {
@@ -190,6 +241,32 @@ class CpfpRepository {
return [];
}
}
// returns `true` if two sets of CPFP clusters are deeply identical
public compareClusters(clustersA: CpfpCluster[], clustersB: CpfpCluster[]): boolean {
if (clustersA.length !== clustersB.length) {
return false;
}
clustersA = clustersA.sort((a,b) => a.root.localeCompare(b.root));
clustersB = clustersB.sort((a,b) => a.root.localeCompare(b.root));
for (let i = 0; i < clustersA.length; i++) {
if (clustersA[i].root !== clustersB[i].root) {
return false;
}
if (clustersA[i].txs.length !== clustersB[i].txs.length) {
return false;
}
for (let j = 0; j < clustersA[i].txs.length; j++) {
if (clustersA[i].txs[j].txid !== clustersB[i].txs[j].txid) {
return false;
}
}
}
return true;
}
}
export default new CpfpRepository();

View File

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

View File

@@ -1,3 +1,4 @@
import config from '../../config';
import { query } from '../../utils/axios-query';
import { ConversionFeed, ConversionRates } from '../price-updater';
@@ -37,15 +38,26 @@ const emptyRates = {
ZAR: -1,
};
class FreeCurrencyApi implements ConversionFeed {
private API_KEY: string;
constructor(apiKey: string) {
this.API_KEY = apiKey;
type PaidCurrencyData = {
[key: string]: {
code: string;
value: number;
}
};
type FreeCurrencyData = {
[key: string]: number;
};
class FreeCurrencyApi implements ConversionFeed {
private API_KEY = config.FIAT_PRICE.API_KEY;
private PAID = config.FIAT_PRICE.PAID;
private API_URL_PREFIX: string = this.PAID ? `https://api.currencyapi.com/v3/` : `https://api.freecurrencyapi.com/v1/`;
constructor() { }
public async $getQuota(): Promise<any> {
const response = await query(`https://api.freecurrencyapi.com/v1/status?apikey=${this.API_KEY}`);
const response = await query(`${this.API_URL_PREFIX}status?apikey=${this.API_KEY}`);
if (response && response['quotas']) {
return response['quotas'];
}
@@ -53,21 +65,36 @@ class FreeCurrencyApi implements ConversionFeed {
}
public async $fetchLatestConversionRates(): Promise<ConversionRates> {
const response = await query(`https://api.freecurrencyapi.com/v1/latest?apikey=${this.API_KEY}`);
const response = await query(`${this.API_URL_PREFIX}latest?apikey=${this.API_KEY}`);
if (response && response['data']) {
if (this.PAID) {
response['data'] = this.convertData(response['data']);
}
return response['data'];
}
return emptyRates;
}
public async $fetchConversionRates(date: string): Promise<ConversionRates> {
const response = await query(`https://api.freecurrencyapi.com/v1/historical?date=${date}&apikey=${this.API_KEY}`);
if (response && response['data'] && response['data'][date]) {
const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`, true);
if (response && response['data'] && (response['data'][date] || this.PAID)) {
if (this.PAID) {
response['data'] = this.convertData(response['data']);
response['data'][response['meta'].last_updated_at.substr(0, 10)] = response['data'];
}
return response['data'][date];
}
return emptyRates;
}
private convertData(data: PaidCurrencyData): FreeCurrencyData {
const simplifiedData: FreeCurrencyData = {};
for (const key in data) {
simplifiedData[key] = data[key].value;
}
return simplifiedData;
}
}
export default FreeCurrencyApi;

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
Signed: bitcoinmechanic

3
contributors/daweilv.txt Normal file
View File

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

View File

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

View File

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

3
contributors/jlopp.txt Normal file
View File

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

View File

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

3
contributors/svrgnty.txt Normal file
View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@
"OFFICIAL": __MEMPOOL_OFFICIAL__,
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
"UNIX_SOCKET_PATH": "__MEMPOOL_UNIX_SOCKET_PATH__",
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
@@ -24,7 +25,7 @@
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
"GOGGLES_INDEXING": __MEMPOOL_GOGGLES_INDEXING__,
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__,
"AUTOMATIC_POOLS_UPDATE": __MEMPOOL_AUTOMATIC_POOLS_UPDATE__,
"AUDIT": __MEMPOOL_AUDIT__,
"RUST_GBT": __MEMPOOL_RUST_GBT__,
"LIMIT_GBT": __MEMPOOL_LIMIT_GBT__,
@@ -59,7 +60,8 @@
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
"FALLBACK": __ESPLORA_FALLBACK__
"FALLBACK": __ESPLORA_FALLBACK__,
"MAX_BEHIND_TIP": __ESPLORA_MAX_BEHIND_TIP__
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
@@ -136,6 +138,8 @@
"ENABLED": __REPLICATION_ENABLED__,
"AUDIT": __REPLICATION_AUDIT__,
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
"STATISTICS": __REPLICATION_STATISTICS__,
"STATISTICS_START_TIME": __REPLICATION_STATISTICS_START_TIME__,
"SERVERS": __REPLICATION_SERVERS__
},
"MEMPOOL_SERVICES": {
@@ -149,6 +153,7 @@
},
"FIAT_PRICE": {
"ENABLED": __FIAT_PRICE_ENABLED__,
"PAID": __FIAT_PRICE_PAID__,
"API_KEY": "__FIAT_PRICE_API_KEY__"
}
}

View File

@@ -7,6 +7,7 @@ __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
__MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false}
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
__MEMPOOL_UNIX_SOCKET_PATH__=${MEMPOOL_UNIX_SOCKET_PATH:=""}
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
@@ -25,11 +26,11 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true}
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
@@ -61,6 +62,7 @@ __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
__ESPLORA_MAX_BEHIND_TIP__=${ESPLORA_MAX_BEHIND_TIP:=2}
# SECOND_CORE_RPC
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
@@ -134,22 +136,25 @@ __MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mm
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
# REPLICATION
__REPLICATION_ENABLED__=${REPLICATION_ENABLED:=true}
__REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
__REPLICATION_ENABLED__=${REPLICATION_ENABLED:=false}
__REPLICATION_AUDIT__=${REPLICATION_AUDIT:=false}
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
__REPLICATION_STATISTICS__=${REPLICATION_STATISTICS:=false}
__REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=1481932800}
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
# FIAT_PRICE
__FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true}
__FIAT_PRICE_PAID__=${FIAT_PRICE_PAID:=false}
__FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@@ -160,6 +165,7 @@ sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!g" mempool-config.json
sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_UNIX_SOCKET_PATH__!${__MEMPOOL_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
@@ -178,7 +184,7 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__!${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
@@ -211,6 +217,7 @@ sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTE
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
sed -i "s!__ESPLORA_MAX_BEHIND_TIP__!${__ESPLORA_MAX_BEHIND_TIP__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json
@@ -281,6 +288,8 @@ sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.jso
sed -i "s!__REPLICATION_ENABLED__!${__REPLICATION_ENABLED__}!g" mempool-config.json
sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
sed -i "s!__REPLICATION_STATISTICS__!${__REPLICATION_STATISTICS__}!g" mempool-config.json
sed -i "s!__REPLICATION_STATISTICS_START_TIME__!${__REPLICATION_STATISTICS_START_TIME__}!g" mempool-config.json
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
# MEMPOOL_SERVICES
@@ -294,6 +303,7 @@ sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g"
# FIAT_PRICE
sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.json
sed -i "s!__FIAT_PRICE_PAID__!${__FIAT_PRICE_PAID__}!g" mempool-config.json
sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json
node /backend/package/index.js

View File

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

View File

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

View File

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

1
frontend/.gitignore vendored
View File

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

View File

@@ -54,6 +54,10 @@
"translation": "src/locale/messages.fr.xlf",
"baseHref": "/fr/"
},
"hr": {
"translation": "src/locale/messages.hr.xlf",
"baseHref": "/hr/"
},
"ja": {
"translation": "src/locale/messages.ja.xlf",
"baseHref": "/ja/"
@@ -166,10 +170,26 @@
"src/resources",
"src/robots.txt",
"src/config.js",
"src/customize.js",
"src/config.template.js"
],
"styles": [
"src/styles.scss",
{
"input": "src/theme-contrast.scss",
"bundleName": "contrast",
"inject": false
},
{
"input": "src/theme-wiz.scss",
"bundleName": "wiz",
"inject": false
},
{
"input": "src/theme-bukele.scss",
"bundleName": "bukele",
"inject": false
},
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
],
"vendorChunk": true,

View File

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

View File

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

View File

@@ -46,7 +46,8 @@ describe('Liquid Testnet', () => {
});
it('loads a specific block page', () => {
cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`);
cy.visit(`${basePath}/block/fb4cbcbff3993ca4bf8caf657d55a23db5ed4ab1cfa33c489303c2e04e1c38e0`);
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
});

View File

@@ -67,7 +67,7 @@ describe('Mainnet', () => {
cy.get('[id^="bitcoin-block-"]').should('have.length', 22);
cy.get('.footer').should('be.visible');
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
expect(text).to.match(/Incoming transactions.* vB\/s/);
expect(text).to.match(/Incoming Transactions.* vB\/s/);
});
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
expect(text).to.match(/Unconfirmed:(.*)/);
@@ -103,6 +103,7 @@ describe('Mainnet', () => {
it('check op_return tx tooltip', () => {
cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover');
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter');
@@ -111,9 +112,10 @@ describe('Mainnet', () => {
it('check op_return coinbase tooltip', () => {
cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('div > a > .badge').first().trigger('onmouseover');
cy.get('div > a > .badge').first().trigger('mouseenter');
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover');
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter');
cy.get('.tooltip-inner').should('be.visible');
});
@@ -142,13 +144,13 @@ describe('Mainnet', () => {
});
});
['BC1PQYQSZQ', 'bc1PqYqSzQ'].forEach((searchTerm) => {
['BC1PQYQS', 'bc1PqYqS'].forEach((searchTerm) => {
it(`allows searching for partial case insensitive bech32m addresses: ${searchTerm}`, () => {
cy.visit('/');
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
cy.get('app-search-results button.dropdown-item').should('have.length', 1);
cy.get('app-search-results button.dropdown-item').should('have.length', 10);
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
cy.url().should('include', '/address/bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e');
cy.url().should('include', '/address/bc1pqyqs26fs4gnyw4aqttyjqa5ta7075zzfjftyz98qa8vdr49dh7fqm2zkv3');
cy.waitForSkeletonGone();
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
});
@@ -156,13 +158,13 @@ describe('Mainnet', () => {
});
});
['BC1Q000375VXCU', 'bC1q000375vXcU'].forEach((searchTerm) => {
['BC1Q0003', 'bC1q0003'].forEach((searchTerm) => {
it(`allows searching for partial case insensitive bech32 addresses: ${searchTerm}`, () => {
cy.visit('/');
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
cy.get('app-search-results button.dropdown-item').should('have.length', 1);
cy.get('app-search-results button.dropdown-item').should('have.length', 10);
cy.get('app-search-results button.dropdown-item.active').click().then(() => {
cy.url().should('include', '/address/bc1q000375vxcuf5v04lmwy22vy2thvhqkxghgq7dy');
cy.url().should('include', '/address/bc1q000303cgr9zazthut63kdktwtatfe206um8nyh');
cy.waitForSkeletonGone();
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
});
@@ -283,6 +285,7 @@ describe('Mainnet', () => {
it('loads genesis block and keypress arrow right', () => {
cy.viewport('macbook-16');
cy.visit('/block/0');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.waitForPageIdle();
@@ -295,6 +298,7 @@ describe('Mainnet', () => {
it('loads genesis block and keypress arrow left', () => {
cy.viewport('macbook-16');
cy.visit('/block/0');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.waitForPageIdle();
@@ -323,6 +327,7 @@ describe('Mainnet', () => {
it('loads genesis block and click on the arrow left', () => {
cy.viewport('macbook-16');
cy.visit('/block/0');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
@@ -339,7 +344,7 @@ describe('Mainnet', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.changeNetwork('testnet');
cy.changeNetwork('testnet4');
cy.changeNetwork('signet');
cy.changeNetwork('mainnet');
});
@@ -439,6 +444,7 @@ describe('Mainnet', () => {
describe('blocks', () => {
it('shows empty blocks properly', () => {
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.get('h2').invoke('text').should('equal', '1 transaction');
@@ -446,6 +452,7 @@ describe('Mainnet', () => {
it('expands and collapses the block details', () => {
cy.visit('/block/0');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.get('.btn.btn-outline-info').click().then(() => {
@@ -458,6 +465,7 @@ describe('Mainnet', () => {
});
it('shows blocks with no pagination', () => {
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions');
@@ -467,6 +475,7 @@ describe('Mainnet', () => {
it('supports pagination on the block screen', () => {
// 41 txs
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('.pagination-container a').invoke('text').then((text1) => {
cy.get('.active + li').first().click().then(() => {
@@ -482,6 +491,7 @@ describe('Mainnet', () => {
it('shows blocks pagination with 5 pages (desktop)', () => {
cy.viewport(760, 800);
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.waitForPageIdle();
});
@@ -493,6 +503,7 @@ describe('Mainnet', () => {
it('shows blocks pagination with 3 pages (mobile)', () => {
cy.viewport(669, 800);
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.waitForPageIdle();
});
@@ -532,16 +543,7 @@ describe('Mainnet', () => {
}
});
cy.get('.alert').should('be.visible');
cy.get('.alert').invoke('css', 'width').then((alertWidth) => {
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
});
cy.get('.btn-warning').then(getRectangle).then((rectA) => {
cy.get('.alert').then(getRectangle).then((rectB) => {
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
});
});
cy.get('.alert-replaced').should('be.visible');
});
it('shows RBF transactions properly (desktop)', () => {

View File

@@ -95,12 +95,14 @@ describe('Signet', () => {
describe('blocks', () => {
it('shows empty blocks properly', () => {
cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('h2').invoke('text').should('equal', '1 transaction');
});
it('expands and collapses the block details', () => {
cy.visit('/signet/block/0');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('be.visible');
@@ -113,6 +115,7 @@ describe('Signet', () => {
it('shows blocks with no pagination', () => {
cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('h2').invoke('text').should('equal', '13 transactions');
cy.get('ul.pagination').first().children().should('have.length', 5);
@@ -121,6 +124,7 @@ describe('Signet', () => {
it('supports pagination on the block screen', () => {
// 43 txs
cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
cy.get('.active + li').first().click().then(() => {

View File

@@ -2,7 +2,7 @@ import { emitMempoolInfo } from '../../support/websocket';
const baseModule = Cypress.env('BASE_MODULE');
describe('Testnet', () => {
describe('Testnet4', () => {
beforeEach(() => {
cy.intercept('/api/block-height/*').as('block-height');
cy.intercept('/api/block/*').as('block');
@@ -13,7 +13,7 @@ describe('Testnet', () => {
if (baseModule === 'mempool') {
it('loads the dashboard', () => {
cy.visit('/testnet');
cy.visit('/testnet4');
cy.waitForSkeletonGone();
});
@@ -25,7 +25,7 @@ describe('Testnet', () => {
it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket();
cy.visit('/testnet');
cy.visit('/testnet4');
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
@@ -45,7 +45,7 @@ describe('Testnet', () => {
});
it('loads the pools screen', () => {
cy.visit('/testnet');
cy.visit('/testnet4');
cy.waitForSkeletonGone();
cy.get('#btn-pools').click().then(() => {
cy.wait(1000);
@@ -53,7 +53,7 @@ describe('Testnet', () => {
});
it('loads the graphs screen', () => {
cy.visit('/testnet');
cy.visit('/testnet4');
cy.waitForSkeletonGone();
cy.get('#btn-graphs').click().then(() => {
cy.wait(1000);
@@ -63,7 +63,7 @@ describe('Testnet', () => {
describe('tv mode', () => {
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/testnet/graphs');
cy.visit('/testnet4/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.wait(1000);
@@ -73,7 +73,7 @@ describe('Testnet', () => {
});
it('loads the tv screen - mobile', () => {
cy.visit('/testnet/graphs');
cy.visit('/testnet4/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('iphone-6');
@@ -85,7 +85,7 @@ describe('Testnet', () => {
it('loads the api screen', () => {
cy.visit('/testnet');
cy.visit('/testnet4');
cy.waitForSkeletonGone();
cy.get('#btn-docs').click().then(() => {
cy.wait(1000);
@@ -94,13 +94,15 @@ describe('Testnet', () => {
describe('blocks', () => {
it('shows empty blocks properly', () => {
cy.visit('/testnet/block/0');
cy.visit('/testnet4/block/0');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('h2').invoke('text').should('equal', '1 transaction');
});
it('expands and collapses the block details', () => {
cy.visit('/testnet/block/0');
cy.visit('/testnet4/block/0');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('be.visible');
@@ -112,15 +114,17 @@ describe('Testnet', () => {
});
it('shows blocks with no pagination', () => {
cy.visit('/testnet/block/000000000000002f8ce27716e74ecc7ad9f7b5101fed12d09e28bb721b9460ea');
cy.visit('/testnet4/block/000000000066e8b6cc78a93f8989587f5819624bae2eb1c05f535cadded19f99');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('h2').invoke('text').should('equal', '11 transactions');
cy.get('h2').invoke('text').should('equal', '18 transactions');
cy.get('ul.pagination').first().children().should('have.length', 5);
});
it('supports pagination on the block screen', () => {
// 48 txs
cy.visit('/testnet/block/000000000000002ca3878ebd98b313a1c2d531f2e70a6575d232ca7564dea7a9');
cy.visit('/testnet4/block/000000000000006982d53f8273bdff21dafc380c292eabc669b5ab6d732311c3');
cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } });
cy.waitForSkeletonGone();
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
cy.get('.active + li').first().click().then(() => {

View File

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

View File

@@ -72,7 +72,7 @@ Cypress.Commands.add('mockMempoolSocket', () => {
mockWebSocket();
});
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "mainnet") => {
Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => {
cy.get('.dropdown-toggle').click().then(() => {
cy.get(`a.${network}`).click().then(() => {
cy.waitForPageIdle();

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "3.0.0-dev",
"version": "3.0.1",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -50,16 +50,16 @@
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
"serve:ssr": "npm run generate-config && node server.run.js",
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
"prerender": "npm run ng -- run mempool:prerender",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:run:record": "cypress run --record",
"cypress:open:ci": "node update-config.js TESTNET_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 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 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 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": "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"
},
"dependencies": {
"@angular-devkit/build-angular": "^17.3.1",
@@ -76,9 +76,9 @@
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.5.1",
"@fortawesome/fontawesome-svg-core": "~6.5.1",
"@fortawesome/free-solid-svg-icons": "~6.5.1",
"@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0",
@@ -88,11 +88,11 @@
"domino": "^2.1.6",
"echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.1.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.20.2",
"esbuild": "^0.23.0",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
@@ -115,7 +115,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.7.0",
"cypress": "^13.13.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@@ -24,7 +24,7 @@ PROXY_CONFIG = [
'/api/**', '!/api/v1/ws',
'!/liquid', '!/liquid/**', '!/liquid/',
'!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/',
'/testnet/api/**', '/signet/api/**'
'/testnet/api/**', '/signet/api/**', '/testnet4/api/**'
],
target: "https://mempool.space",
ws: true,

View File

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

View File

@@ -7,6 +7,9 @@ import { MempoolBlockViewComponent } from './components/mempool-block-view/mempo
import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
import { AddressGroupComponent } from './components/address-group/address-group.component';
import { TrackerComponent } from './components/tracker/tracker.component';
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from './route-guards';
const browserWindow = window || {};
// @ts-ignore
@@ -51,6 +54,44 @@ let routes: Routes = [
},
]
},
{
path: 'testnet4',
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
children: [],
component: AddressGroupComponent,
data: {
networkSpecific: true,
}
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
component: StatusViewComponent
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: '/testnet4'
},
]
},
{
path: 'signet',
children: [
@@ -100,6 +141,12 @@ let routes: Routes = [
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: 'tx',
canMatch: [TrackerGuard],
runGuardsAndResolvers: 'always',
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
@@ -124,6 +171,10 @@ let routes: Routes = [
path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet4',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
},
{
path: 'signet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
@@ -164,10 +215,6 @@ let routes: Routes = [
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: ''
},
];
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
@@ -252,18 +299,21 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '**',
redirectTo: ''
},
];
}
if (!window['isMempoolSpaceBuild']) {
routes.push({
path: '**',
redirectTo: ''
});
}
@NgModule({
imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabledBlocking',
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
anchorScrolling: 'disabled',
preloadingStrategy: AppPreloadingStrategy
})],
})

View File

@@ -1,4 +1,4 @@
export const mempoolFeeColors = [
export const defaultMempoolFeeColors = [
'557d00',
'5d7d01',
'637d02',
@@ -39,6 +39,47 @@ export const mempoolFeeColors = [
'ae005b',
];
export const contrastMempoolFeeColors = [
'0082e6',
'0984df',
'1285d9',
'1a87d2',
'2388cb',
'2c8ac5',
'358bbe',
'3e8db7',
'468eb0',
'4f90aa',
'5892a3',
'61939c',
'6a9596',
'72968f',
'7b9888',
'849982',
'8d9b7b',
'959c74',
'9e9e6e',
'a79f67',
'b0a160',
'b9a35a',
'c1a453',
'caa64c',
'd3a745',
'dca93f',
'e5aa38',
'edac31',
'f6ad2b',
'ffaf24',
'ffb01e',
'ffb118',
'ffb212',
'ffb30c',
'ffb406',
'ffb500',
'ffb600',
'ffb700',
];
export const chartColors = [
"#D81B60",
"#8E24AA",
@@ -110,7 +151,7 @@ export const languages: Language[] = [
{ code: 'fr', name: 'Français' }, // French
// { code: 'gl', name: 'Galego' }, // Galician
{ code: 'ko', name: '한국어' }, // Korean
// { code: 'hr', name: 'Hrvatski' }, // Croatian
{ code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese
@@ -148,22 +189,22 @@ export const specialBlocks = {
'0': {
labelEvent: 'Genesis',
labelEventCompleted: 'The Genesis of Bitcoin',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'210000': {
labelEvent: 'Bitcoin\'s 1st Halving',
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'420000': {
labelEvent: 'Bitcoin\'s 2nd Halving',
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'630000': {
labelEvent: 'Bitcoin\'s 3rd Halving',
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'709632': {
labelEvent: 'Taproot 🌱 activation',
@@ -173,62 +214,62 @@ export const specialBlocks = {
'840000': {
labelEvent: 'Bitcoin\'s 4th Halving',
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1050000': {
labelEvent: 'Bitcoin\'s 5th Halving',
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1260000': {
labelEvent: 'Bitcoin\'s 6th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1470000': {
labelEvent: 'Bitcoin\'s 7th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1680000': {
labelEvent: 'Bitcoin\'s 8th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'1890000': {
labelEvent: 'Bitcoin\'s 9th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2100000': {
labelEvent: 'Bitcoin\'s 10th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2310000': {
labelEvent: 'Bitcoin\'s 11th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2520000': {
labelEvent: 'Bitcoin\'s 12th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2730000': {
labelEvent: 'Bitcoin\'s 13th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'2940000': {
labelEvent: 'Bitcoin\'s 14th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
},
'3150000': {
labelEvent: 'Bitcoin\'s 15th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
networks: ['mainnet', 'testnet'],
networks: ['mainnet', 'testnet', 'testnet4'],
}
};

View File

@@ -12,6 +12,7 @@ import { PriceService } from './services/price.service';
import { EnterpriseService } from './services/enterprise.service';
import { WebsocketService } from './services/websocket.service';
import { AudioService } from './services/audio.service';
import { PreloadService } from './services/preload.service';
import { SeoService } from './services/seo.service';
import { OpenGraphService } from './services/opengraph.service';
import { ZoneService } from './services/zone-shim.service';
@@ -19,12 +20,14 @@ import { SharedModule } from './shared/shared.module';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { LanguageService } from './services/language.service';
import { ThemeService } from './services/theme.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy';
import { ServicesApiServices } from './services/services-api.service';
import { DatePipe } from '@angular/common';
const providers = [
ElectrsApiService,
@@ -38,12 +41,15 @@ const providers = [
StorageService,
EnterpriseService,
LanguageService,
ThemeService,
ShortenStringPipe,
FiatShortenerPipe,
FiatCurrencyPipe,
CapAddressPipe,
DatePipe,
AppPreloadingStrategy,
ServicesApiServices,
PreloadService,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
{ provide: ZONE_SERVICE, useClass: ZoneService },
];

View File

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

View File

@@ -14,7 +14,7 @@
}
.become-sponsor {
background-color: #1d1f31;
background-color: var(--bg);
border-radius: 16px;
padding: 12px 20px;
width: 400px;

View File

@@ -32,7 +32,7 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<ng-container *ngIf="officialMempoolSpace">
<ng-container>
<app-about-sponsors></app-about-sponsors>
</ng-container>
@@ -53,13 +53,26 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-10 -10 100 100" class="image">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-186.000000, -2316.000000)">
<g transform="translate(186.000000, 2316.000000)">
<rect id="" fill="#023D32" x="-10" y="-10" width="100" height="100" rx="8"></rect>
<path d="M61.6666667,9.16666667 L61.6666667,17.0041667 L46.2625,17.0041667 C46.2625,17.0041667 44.1666667,16.6666667 44.1666667,18.3333333 L44.1666667,25.8025 L61.6666667,25.8025 L61.6666667,34.7391667 L44.1666667,34.7391667 L44.1666667,70.5575 L31.7825,70.5575 L31.7825,35 L19.1666667,35 L19.1666667,25.595 L31.6666667,25.595 L31.6666667,17.5 C31.6666667,17.5 32.5,9.16666667 40.4166667,9.16666667 L61.6666667,9.16666667 Z" id="Fill-1" fill="#86E2A0"></path>
</g>
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76" class="image">
<defs>
<style>
.d {
fill: #fff;
}
.e {
fill: #ff8200;
}
</style>
</defs>
<g id="c" data-name="b">
<circle class="e" cx="24" cy="32" r="8" />
<circle class="e" cx="24" cy="56" r="8" />
<circle class="e" cx="8" cy="68" r="8" />
<g>
<circle class="d" cx="24" cy="8" r="8" />
<circle class="d" cx="8" cy="20" r="8" />
<circle class="d" cx="8" cy="44" r="8" />
</g>
</g>
</svg>
@@ -112,7 +125,9 @@
<span>Blockstream</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
</svg>
<span>Unchained</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
@@ -137,7 +152,7 @@
<span>Bull Bitcoin</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)">
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
@@ -181,15 +196,15 @@
</div>
</div>
<ng-container *ngIf="officialMempoolSpace">
<ng-container>
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper">
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@@ -200,8 +215,8 @@
<h3 i18n="about.sponsors.withHeart">Chad Sponsors</h3>
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -213,8 +228,8 @@
<h3 i18n="about.sponsors.withHeart">OG Sponsors ❤️</h3>
<div class="wrapper">
<ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
<a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<a *ngFor="let ogSponsor of ogs" [href]="'https://x.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-container>
</div>
@@ -259,22 +274,10 @@
<img class="image" src="/resources/profile/bisq_network.png" />
<span>Bisq</span>
</a>
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
<img class="image" src="/resources/profile/bluewallet.png" />
<span>BlueWallet</span>
</a>
<a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
<img class="image" src="/resources/profile/muun.png" />
<span>Muun</span>
</a>
<a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
<img class="image" src="/resources/profile/electrum.png" />
<span>Electrum</span>
</a>
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
<img class="image" src="/resources/profile/specter.png" />
<span>Specter</span>
</a>
<a href="https://github.com/sparrowwallet/sparrow" target="_blank" title="Sparrow Wallet">
<img class="image" src="/resources/profile/sparrow.png" />
<span>Sparrow</span>
@@ -283,21 +286,37 @@
<img class="image not-rounded" src="/resources/profile/phoenix.svg" />
<span>Phoenix</span>
</a>
<a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits">
<img class="image" src="/resources/profile/lnbits.svg" />
<span>LNBits</span>
<a href="http://github.com/COLDCARD" target="_blank" title="COLDCARD">
<img class="image coldcard" src="/resources/profile/coldcard.png" />
<span>COLDCARD</span>
</a>
<a href="https://github.com/layer2tech/mercury-wallet" target="_blank" title="Mercury Wallet">
<img class="image" src="/resources/profile/mercury.svg" />
<span>Mercury</span>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
<img class="image" src="/resources/profile/zeus.png" />
<span>ZEUS</span>
</a>
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
<span>Mutiny</span>
</a>
<a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet">
<img class="image" src="/resources/profile/blixt.png" />
<span>Blixt</span>
</a>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
<img class="image" src="/resources/profile/zeus.png" />
<span>ZEUS</span>
<a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck">
<img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span>
</a>
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
<img class="image" src="/resources/profile/bluewallet.png" />
<span>BlueWallet</span>
</a>
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
<img class="image" src="/resources/profile/boltz.svg" />
<span>Boltz</span>
</a>
<a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits">
<img class="image" src="/resources/profile/lnbits.svg" />
<span>LNBits</span>
</a>
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
<img class="image" src="/resources/profile/marina.svg" />
@@ -307,13 +326,9 @@
<img class="image" src="/resources/profile/schildbach.svg" />
<span>Schildbach</span>
</a>
<a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck">
<img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span>
</a>
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
<img class="image" src="/resources/profile/bitcoin-s.svg" />
<span>bitcoin-s</span>
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
<img class="image" src="/resources/profile/specter.png" />
<span>Specter</span>
</a>
<a href="https://github.com/EdgeApp" target="_blank" title="Edge">
<img class="image not-rounded" src="/resources/profile/edge.svg" />
@@ -323,13 +338,13 @@
<img class="image" src="/resources/profile/galoy.svg" />
<span>Galoy</span>
</a>
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
<img class="image" src="/resources/profile/boltz.svg" />
<span>Boltz</span>
<a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
<img class="image" src="/resources/profile/muun.png" />
<span>Muun</span>
</a>
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
<span>Mutiny</span>
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
<img class="image" src="/resources/profile/bitcoin-s.svg" />
<span>bitcoin-s</span>
</a>
</div>
</div>
@@ -343,8 +358,8 @@
<a href="https://opencrypto.org/" title="Coppa - Crypto Open Patent Alliance">
<img class="copa" src="/resources/profile/copa.png" />
</a>
<a href="https://bisq.network/" title="Bisq Network">
<img class="bisq" src="/resources/profile/bisq.svg" />
<a href="https://bitcoin.gob.sv" title="Oficina Nacional del Bitcoin">
<img class="sv" src="/resources/profile/onbtc-full.svg" />
</a>
</div>
</div>
@@ -354,8 +369,8 @@
<h3 i18n="about.translators">Project Translators</h3>
<div class="wrapper">
<ng-template ngFor let-translator [ngForOf]="translators">
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<a [href]="'https://x.com/' + translator.value" target="_blank" [title]="translator.key">
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -369,7 +384,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.regular">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>
@@ -380,8 +395,8 @@
<h3 i18n="about.project_members">Project Members</h3>
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.core">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name" [class]="'project-member-avatar'">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>
@@ -392,11 +407,11 @@
<div class="maintainers" id="project-maintainers">
<h3 i18n="about.maintainers">Project Maintainers</h3>
<div class="wrapper">
<a href="https://twitter.com/softsimon_" target="_blank" title="softsimon">
<a href="https://x.com/softsimon_" target="_blank" title="softsimon">
<img class="image" src="/resources/profile/softsimon.jpg" />
<span>softsimon</span>
</a>
<a href="https://twitter.com/wiz" target="_blank" title="wiz">
<a href="https://x.com/wiz" target="_blank" title="wiz">
<img class="image" src="/resources/profile/wiz.png" />
<span>wiz</span>
</a>
@@ -422,7 +437,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@@ -10,14 +10,9 @@
margin: 25px;
line-height: 32px;
}
.unknown {
border: 1px solid #b4b4b4;
}
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro {
@@ -129,8 +124,9 @@
position: relative;
width: 300px;
}
.bisq {
top: 3px;
.sv {
height: 85px;
width: auto;
position: relative;
}
}
@@ -158,6 +154,11 @@
}
img, svg {
margin: 40px 29px 10px;
&.image.coldcard {
border-radius: 0;
height: auto;
margin: 20px 29px 20px;
}
}
}
}
@@ -177,6 +178,10 @@
}
}
#project-members a.project-member-avatar img {
margin: 40px 20px 10px;
}
.copyright {
text-align: left;
max-width: 620px;

View File

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

View File

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

View File

@@ -0,0 +1,879 @@
/* eslint-disable no-console */
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '../../services/services-api.service';
import { md5, insecureRandomUUID } from '../../shared/common.utils';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ETA, EtaService } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { MiningStats } from '../../services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service';
import { ApiService } from '../../services/api.service';
import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
export type AccelerationEstimate = {
hasAccess: boolean;
txSummary: TxSummary;
nextBlockFee: number;
targetFeeRate: number;
userBalance: number;
enoughBalance: boolean;
cost: number;
mempoolBaseFee: number;
vsizeFee: number;
pools: number[];
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
unavailable?: boolean;
options: { // recommended bid options
fee: number; // recommended userBid in sats
}[];
}
export type TxSummary = {
txid: string; // txid of the current transaction
effectiveVsize: number; // Total vsize of the dependency tree
effectiveFee: number; // Total fee of the dependency tree in sats
ancestorCount: number; // Number of ancestors
}
export interface RateOption {
fee: number;
rate: number;
index: number;
}
export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
@Component({
selector: 'app-accelerate-checkout',
templateUrl: './accelerate-checkout.component.html',
styleUrls: ['./accelerate-checkout.component.scss']
})
export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() tx: Transaction;
@Input() accelerating: boolean = false;
@Input() miningStats: MiningStats;
@Input() eta: ETA;
@Input() scrollEvent: boolean;
@Input() cashappEnabled: boolean = true;
@Input() applePayEnabled: boolean = false;
@Input() googlePayEnabled: boolean = true;
@Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false;
@Input() noCTA: boolean = false;
@Output() unavailable = new EventEmitter<boolean>();
@Output() completed = new EventEmitter<boolean>();
@Output() hasDetails = new EventEmitter<boolean>();
@Output() changeMode = new EventEmitter<boolean>();
calculating = true;
selectedOption: 'wait' | 'accel';
cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data
accelerateError = ''; // error executing acceleration
btcpayInvoiceFailed = false;
timePaid: number = 0; // time acceleration requested
math = Math;
isMobile: boolean = window.innerWidth <= 767.98;
isProdDomain = ['mempool.space',
'mempool-staging.va1.mempool.space',
'mempool-staging.fmt.mempool.space',
'mempool-staging.fra.mempool.space',
'mempool-staging.tk7.mempool.space',
'mempool-staging.sg1.mempool.space'
].indexOf(document.location.hostname) > -1;
private _step: CheckoutStep = 'summary';
simpleMode: boolean = true;
timeoutTimer: any;
authSubscription$: Subscription;
auth: IAuth | null = null;
// accelerator stuff
accelerationUUID: string;
accelerationSubscription: Subscription;
difficultySubscription: Subscription;
estimateSubscription: Subscription;
estimate: AccelerationEstimate;
maxBidBoost: number; // sats
cost: number; // sats
etaInfo$: Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }>;
showSuccess = false;
hasAncestors: boolean = false;
minExtraCost = 0;
minBidAllowed = 0;
maxBidAllowed = 0;
defaultBid = 0;
userBid = 0;
selectFeeRateIndex = 1;
maxRateOptions: RateOption[] = [];
// square
loadingCashapp = false;
loadingApplePay = false;
loadingGooglePay = false;
payments: any;
cashAppPay: any;
applePay: any;
googlePay: any;
conversionsSubscription: Subscription;
conversions: Record<string, number>;
// btcpay
loadingBtcpayInvoice = false;
invoice = undefined;
constructor(
public stateService: StateService,
private apiService: ApiService,
private servicesApiService: ServicesApiServices,
private etaService: EtaService,
private audioService: AudioService,
private cd: ChangeDetectorRef,
private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService,
) {
this.accelerationUUID = insecureRandomUUID();
// Check if Apple Pay available
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
if (window['ApplePaySession']) {
this.applePayEnabled = true;
}
}
ngOnInit(): void {
this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => {
if (this.auth?.user?.userId !== auth?.user?.userId) {
this.auth = auth;
this.estimate = null;
this.quoteError = null;
this.accelerateError = null;
this.timePaid = 0;
this.btcpayInvoiceFailed = false;
this.moveToStep('summary');
} else {
this.auth = auth;
}
});
this.authService.refreshAuth$().subscribe();
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.moveToStep('processing');
this.insertSquare();
this.setupSquare();
} else {
this.moveToStep('summary');
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
}
);
}
ngOnDestroy(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
if (this.authSubscription$) {
this.authSubscription$.unsubscribe();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent && this.scrollEvent) {
this.scrollToElement('acceleratePreviewAnchor', 'start');
}
if (changes.accelerating) {
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
this.moveToStep('success');
}
}
}
moveToStep(step: CheckoutStep): void {
this._step = step;
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
}
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
this.fetchEstimate();
}
if (this._step === 'checkout') {
this.insertSquare();
this.enterpriseService.goal(8);
}
if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.btcpayInvoiceFailed = false;
this.loadingBtcpayInvoice = true;
this.invoice = null;
this.requestBTCPayInvoice();
} else if (this._step === 'cashapp' && this.cashappEnabled) {
this.loadingCashapp = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'applepay' && this.applePayEnabled) {
this.loadingApplePay = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'googlepay' && this.googlePayEnabled) {
this.loadingGooglePay = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'paid') {
this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => {
if (this.step === 'paid') {
this.accelerateError = 'internal_server_error';
}
}, 120000);
}
this.hasDetails.emit(this._step === 'quote');
}
closeModal(): void {
this.completed.emit(true);
this.moveToStep('summary');
}
/**
* Scroll to element id with or without setTimeout
*/
scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void {
setTimeout(() => {
this.scrollToElement(id, position);
}, timeout);
}
scrollToElement(id: string, position: ScrollLogicalPosition): void {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
this.cd.markForCheck();
acceleratePreviewAnchor.scrollIntoView({
behavior: 'smooth',
inline: position,
block: position,
});
}
}
/**
* Accelerator
*/
fetchEstimate(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
this.calculating = true;
this.quoteError = null;
this.accelerateError = null;
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
this.unavailable.emit(true);
}
} else {
this.estimate = response.body;
if (!this.estimate) {
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
this.unavailable.emit(true);
}
return;
}
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.quoteError = `not_enough_balance`;
}
}
if (this.estimate.unavailable) {
this.quoteError = `temporarily_unavailable`;
}
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate, this.miningStats);
this.maxRateOptions = this.estimate.options.map((option, index) => ({
fee: option.fee,
rate: (this.estimate.txSummary.effectiveFee + option.fee) / this.estimate.txSummary.effectiveVsize,
index
}));
this.defaultBid = this.maxRateOptions[1].fee;
this.userBid = this.defaultBid;
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.validateChoice();
if (!this.couldPay) {
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
this.unavailable.emit(true);
}
return;
}
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
this.loadingBtcpayInvoice = true;
this.requestBTCPayInvoice();
}
this.calculating = false;
this.cd.markForCheck();
}
}),
catchError(() => {
this.estimate = undefined;
this.quoteError = `cannot_accelerate_tx`;
this.estimateSubscription.unsubscribe();
if (this.step === 'summary') {
this.unavailable.emit(true);
} else {
this.accelerateError = 'cannot_accelerate_tx';
}
return of(null);
})
).subscribe();
}
validateChoice(): void {
if (!this.canPay) {
if (this.estimate?.availablePaymentMethods?.balance) {
if (this.cost >= this.estimate?.userBalance) {
this.cantPayReason = 'not_enough_balance';
}
} else {
this.cantPayReason = 'cannot_accelerate_tx';
}
} else {
this.cantPayReason = '';
}
}
/**
* User changed his bid
*/
setUserBid({ fee, index }: { fee: number, index: number}): void {
if (this.estimate) {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
}
}
/**
* Account-based acceleration request
*/
accelerateWithMempoolAccount(): void {
if (!this.canPay || this.calculating) {
return;
}
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid,
this.userBid,
this.accelerationUUID
).subscribe({
next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.estimateSubscription.unsubscribe();
this.moveToStep('paid');
},
error: (response) => {
this.accelerateError = response.error;
}
});
}
/**
* Square
*/
insertSquare(): void {
if (!this.isProdDomain && !isDevMode()) {
return;
}
if (window['Square']) {
return;
}
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
if (this.isProdDomain) {
statsUrl = '/square/v1/square.js';
}
(function(): void {
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s);
})();
}
setupSquare(): void {
if (!this.isProdDomain && !isDevMode()) {
return;
}
const init = (): void => {
this.initSquare();
};
if (!window['Square']) {
console.debug('Square.js failed to load properly. Retrying.');
setTimeout(this.setupSquare.bind(this), 100);
} else {
init();
}
}
async initSquare(): Promise<void> {
try {
this.servicesApiService.setupSquare$().subscribe({
next: async (ids) => {
this.payments = window['Square'].payments(ids.squareAppId, ids.squareLocationId);
const urlParams = new URLSearchParams(window.location.search);
if (this._step === 'cashapp' || urlParams.get('cash_request_id')) {
await this.requestCashAppPayment();
} else if (this._step === 'applepay') {
await this.requestApplePayPayment();
} else if (this._step === 'googlepay') {
await this.requestGooglePayPayment();
}
},
error: () => {
console.debug('Error loading Square Payments');
this.accelerateError = 'cannot_setup_square';
}
});
} catch (e) {
console.debug('Error loading Square Payments', e);
this.accelerateError = 'cannot_setup_square';
}
}
/**
* APPLE PAY
*/
async requestApplePayPayment(): Promise<void> {
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
if (this.applePay) {
this.applePay.destroy();
}
const costUSD = this.cost / 100_000_000 * conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
total: {
amount: costUSD.toFixed(2),
label: 'Total',
},
});
try {
this.applePay = await this.payments.applePay(paymentRequest);
const applePayButton = document.getElementById('apple-pay-button');
if (!applePayButton) {
console.error(`Unable to find apple pay button id='apple-pay-button'`);
// Try again
setTimeout(this.requestApplePayPayment.bind(this), 500);
return;
}
this.loadingApplePay = false;
applePayButton.addEventListener('click', async event => {
event.preventDefault();
const tokenResult = await this.applePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithApplePay$(
this.tx.txid,
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID
).subscribe({
next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
this.applePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
}
});
} else {
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
}
});
} catch (e) {
console.error(e);
}
}
);
}
/**
* GOOGLE PAY
*/
async requestGooglePayPayment(): Promise<void> {
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
if (this.googlePay) {
this.googlePay.destroy();
}
const costUSD = this.cost / 100_000_000 * conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
total: {
amount: costUSD.toFixed(2),
label: 'Total'
}
});
this.googlePay = await this.payments.googlePay(paymentRequest , {
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
});
await this.googlePay.attach(`#google-pay-button`, {
buttonType: 'pay',
buttonSizeMode: 'fill',
});
this.loadingGooglePay = false;
document.getElementById('google-pay-button').addEventListener('click', async event => {
event.preventDefault();
const tokenResult = await this.googlePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid,
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID
).subscribe({
next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
this.googlePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
}
});
} else {
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
}
});
}
);
}
/**
* CASHAPP
*/
async requestCashAppPayment(): Promise<void> {
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
const costUSD = this.step === 'processing' ? 69.69 : (this.cost / 100_000_000 * conversions.USD); // When we're redirected to this component, the payment data is already linked to the payment token, so does not matter what amonut we put in there, therefore it's 69.69
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
total: {
amount: costUSD.toFixed(2),
label: 'Total',
pending: true,
productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
}
});
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
redirectURL: `${redirectHostname}/tx/${this.tx.txid}`,
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
});
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
this.loadingCashapp = false;
this.cashAppPay.addEventListener('ontokenization', event => {
const { tokenResult, error } = event.detail;
if (error) {
this.accelerateError = error;
} else if (tokenResult.status === 'OK') {
this.servicesApiService.accelerateWithCashApp$(
this.tx.txid,
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
this.accelerationUUID
).subscribe({
next: () => {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
}
}, 1000);
},
error: (response) => {
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
}
});
}
});
}
);
}
/**
* BTCPay
*/
async requestBTCPayInvoice(): Promise<void> {
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
}),
catchError(error => {
console.log(error);
this.btcpayInvoiceFailed = true;
return of(null);
})
).subscribe((invoice) => {
this.invoice = invoice;
this.cd.markForCheck();
});
}
bitcoinPaymentCompleted(): void {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe();
this.moveToStep('paid');
}
isLoggedIn(): boolean {
return this.auth !== null;
}
/**
* UI events
*/
selectedOptionChanged(event): void {
this.selectedOption = event.target.id;
}
get step(): CheckoutStep {
return this._step;
}
get paymentMethods(): PaymentMethod[] {
return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[];
}
get couldPayWithBitcoin(): boolean {
return !!this.estimate?.availablePaymentMethods?.bitcoin;
}
get couldPayWithCashapp(): boolean {
if (!this.cashappEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.cashapp;
}
get couldPayWithApplePay(): boolean {
if (!this.applePayEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.applePay;
}
get couldPayWithGooglePay(): boolean {
if (!this.googlePayEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.googlePay;
}
get couldPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.balance;
}
get couldPay(): boolean {
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay || this.couldPayWithGooglePay;
}
get canPayWithBitcoin(): boolean {
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
}
get canPayWithCashapp(): boolean {
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.cashapp;
if (paymentMethod) {
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
return true;
}
}
return false;
}
get canPayWithApplePay(): boolean {
if (!this.applePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.applePay;
if (paymentMethod) {
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
return true;
}
}
return false;
}
get canPayWithGooglePay(): boolean {
if (!this.googlePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.googlePay;
if (paymentMethod) {
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
return true;
}
}
return false;
}
get canPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.balance;
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max && this.cost <= this.estimate?.userBalance;
}
get canPay(): boolean {
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay || this.canPayWithGooglePay;
}
get hasAccessToBalanceMode(): boolean {
return this.isLoggedIn() && this.estimate?.hasAccess;
}
get timeSincePaid(): number {
return Date.now() - this.timePaid;
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}

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