Compare commits

...

285 Commits

Author SHA1 Message Date
Mononaut
81ab575bce multiblock fix padding, add loading spinner 2024-10-18 02:33:03 +00:00
Mononaut
a48b631012 cleanup multi-mined-blocks 2024-10-17 10:12:54 +00:00
Mononaut
6928c0aa87 multiblock defaults & resize handler 2024-10-17 10:12:54 +00:00
Mononaut
2e665d57ac multiblock goggles 2024-10-17 10:12:53 +00:00
Mononaut
580ac889df multiblock interactivity 2024-10-17 10:12:53 +00:00
Mononaut
f7422f29dc flip multiblock mempool 2024-10-17 10:12:53 +00:00
Mononaut
d2b918cf15 multiblock padding 2024-10-17 10:12:53 +00:00
Mononaut
17db3d9004 multiblock fix stale mempool txs 2024-10-17 10:12:52 +00:00
Mononaut
d815ab1830 multiblock logos & fix regular visualization 2024-10-17 10:12:52 +00:00
Mononaut
3a0edc6133 multiblock label overlays 2024-10-17 10:12:52 +00:00
Mononaut
afffb04b00 multiblock better scene bounds 2024-10-17 10:12:52 +00:00
Mononaut
50eb9b602b multiblock mempool page 2024-10-17 10:12:52 +00:00
Mononaut
5429d6f264 multiblock support >8 blocks 2024-10-17 10:12:51 +00:00
Mononaut
e63adbe28b multiblock enforce block boundaries 2024-10-17 10:12:51 +00:00
Mononaut
1da6123332 Test rendering multiple blocks on one canvas 2024-10-17 10:12:51 +00:00
wiz
ca7221f8b7 Merge pull request #5594 from mempool/nymkappa/revalidate-accel
[accelerator] revalidate user choice after choosing fee option
2024-10-17 15:09:31 +09:00
wiz
8a579cc374 Merge pull request #5595 from mempool/nymkappa/hashrate-1w
[mining] use getNetworkHashPs(1008)
2024-10-17 15:08:47 +09:00
softsimon
b454959acd Merge pull request #5598 from mempool/dependabot/npm_and_yarn/frontend/tslib-2.8.0
Bump tslib from 2.7.0 to 2.8.0 in /frontend
2024-10-16 14:19:27 +09:00
dependabot[bot]
4498e14be8 Bump tslib from 2.7.0 to 2.8.0 in /frontend
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/v2.7.0...v2.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-16 02:51:18 +00:00
wiz
f27a9a3c50 Merge pull request #5597 from mempool/junderw/llvm17
Use llvm17 because C sucks
2024-10-15 21:19:24 +09:00
junderw
1f0b597e2f Use llvm17 because C sucks 2024-10-15 19:43:56 +09:00
wiz
a3884b95b8 Merge pull request #5593 from mempool/simon/whale-size-increase
Whale size increase
2024-10-14 19:58:02 +09:00
softsimon
f67687b573 Merge pull request #5596 from mempool/simon/taproot-wizards
Add taproot wizards as enterprise sponsor
2024-10-14 19:54:26 +09:00
softsimon
7f4dc7eb3e Add taproot wizards as enterprise sponsor 2024-10-14 19:42:32 +09:00
nymkappa
1c4be164dd [mining] use getNetworkHashPs(1008) 2024-10-14 17:03:52 +09:00
nymkappa
450d83461c [accelerator] revalidate user choice after choosing fee option 2024-10-14 14:49:53 +09:00
softsimon
5f222f59a7 Whale size increase 2024-10-14 14:47:12 +09:00
softsimon
8dac5cff9a Merge pull request #5591 from mempool/dependabot/npm_and_yarn/frontend/multi-d3b9e25284
Bump send and browser-sync in /frontend
2024-10-14 11:33:35 +09:00
softsimon
83c7b3034b Merge pull request #5589 from mempool/dependabot/npm_and_yarn/backend/multi-9f37c16f8f
Bump cookie and express in /backend
2024-10-14 11:32:45 +09:00
dependabot[bot]
ce1babf67b Bump send and browser-sync in /frontend
Bumps [send](https://github.com/pillarjs/send) to 0.19.0 and updates ancestor dependency [browser-sync](https://github.com/BrowserSync/browser-sync). These dependencies need to be updated together.


Updates `send` from 0.16.2 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.16.2...0.19.0)

Updates `browser-sync` from 3.0.2 to 3.0.3
- [Release notes](https://github.com/BrowserSync/browser-sync/releases)
- [Changelog](https://github.com/BrowserSync/browser-sync/blob/master/changelog.js)
- [Commits](https://github.com/BrowserSync/browser-sync/compare/v3.0.2...v3.0.3)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: browser-sync
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 02:28:52 +00:00
softsimon
7ea921a5cb Merge pull request #5590 from mempool/dependabot/npm_and_yarn/frontend/multi-2c5a3fe122
Bump cookie, socket.io and express in /frontend
2024-10-14 11:28:06 +09:00
softsimon
26e3a2413d Merge pull request #5567 from mempool/natsoni/block-first-seen-audit
Store first seen time in block audit
2024-10-14 10:10:21 +09:00
wiz
8ad6c93e92 Merge pull request #5587 from mempool/simon/bump-core-28
Bump Core to v28.0
2024-10-13 23:25:34 +09:00
natsoni
198d79f149 Merge branch 'master' into natsoni/block-first-seen-audit 2024-10-13 17:41:56 +09:00
dependabot[bot]
8a72a5871d Bump cookie, socket.io and express in /frontend
Bumps [cookie](https://github.com/jshttp/cookie), [socket.io](https://github.com/socketio/socket.io) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `socket.io` from 4.7.1 to 4.8.0
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/4.7.1...socket.io@4.8.0)

Updates `express` from 4.21.0 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: socket.io
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-13 07:51:12 +00:00
dependabot[bot]
2c12f890bd Bump cookie and express in /backend
Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.1 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `express` from 4.21.0 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-13 07:50:05 +00:00
softsimon
f9300130fe Bump Core to v28.0 2024-10-13 16:00:08 +09:00
softsimon
5b557b2c12 Merge pull request #5586 from mempool/natsoni/search-bar-seconds
Include optional seconds in search bar date
2024-10-13 15:28:49 +09:00
natsoni
071e9b6c2c Include optional seconds in search bar date 2024-10-13 12:54:58 +09:00
wiz
f78971e640 Merge pull request #5547 from Emzy/ops/add-testnet4
Add testnet4 to install script
2024-10-13 11:54:05 +09:00
wiz
b86c8f7976 Merge pull request #5585 from mempool/natsoni/submit-package
Add package broadcaster to tx push page
2024-10-13 11:30:57 +09:00
wiz
2ce596a14b Merge branch 'master' into natsoni/submit-package 2024-10-13 11:15:24 +09:00
natsoni
735ed87b78 Route submitpackage calls to core on esplora backends 2024-10-13 11:14:23 +09:00
natsoni
d1741a51c9 Add submit package option to tx push page 2024-10-12 17:38:48 +09:00
natsoni
9f0b3bd769 Add submitpackage endpoint 2024-10-12 17:38:37 +09:00
wiz
41088cca09 Merge pull request #5507 from mempool/nymkappa/faucet-unverified
[faucet] show unverified warning if no email provided
2024-10-12 17:10:53 +09:00
wiz
e92ffbd501 Merge branch 'master' into nymkappa/faucet-unverified 2024-10-12 17:09:07 +09:00
natsoni
93d9538845 Fix error formatting on core only backend 2024-10-12 15:56:38 +09:00
softsimon
ae46fcafb9 Merge pull request #5583 from mempool/natsoni/inscriptions-license
Add license to inscriptions.utils.ts
2024-10-10 20:56:02 +09:00
natsoni
69a994afd5 Add license to inscriptions.utils.ts 2024-10-10 20:53:19 +09:00
wiz
c6cc533baa Merge pull request #5582 from mempool/simon/set-audit-start-heights-prod
Set audit start heights on prod
2024-10-10 20:46:10 +09:00
natsoni
dd0542bbe1 Store block first seen in db 2024-10-10 18:47:07 +09:00
softsimon
cdb4580c6d Set audit start heights on prod 2024-10-10 18:01:35 +09:00
softsimon
fe4b39df80 Merge pull request #5483 from mempool/natsoni/handle-city-states
Handle city-states in geolocation component
2024-10-10 16:22:48 +09:00
wiz
1a7519dd00 Merge pull request #5576 from mempool/nymkappa/fix-simple-mode-amount-charged
[accelerator] fee delta matches what the user accepted to pay in frontend
2024-10-10 15:59:41 +09:00
softsimon
5116a27e8d Merge pull request #5581 from mempool/natsoni/fix-timeline-again
Fix timespan on acceleration timeline
2024-10-10 10:23:41 +09:00
natsoni
73e8ba3e47 Fix timestamps on acceleration timeline 2024-10-09 20:52:23 +09:00
softsimon
6805b673fa Merge pull request #5580 from mempool/natsoni/fix-frontend
Fix frontend build
2024-10-09 18:18:25 +09:00
natsoni
22236bdabe Fix frontend build 2024-10-09 18:17:17 +09:00
softsimon
05f60cda56 Merge pull request #5578 from mempool/natsoni/fix-timeline
Fix wrong timespan on acceleration timeline
2024-10-09 17:52:51 +09:00
natsoni
c4004ba301 Clean up timeline code 2024-10-09 17:50:24 +09:00
natsoni
15b7e75b69 Fix wrong timespan in acc timeline 2024-10-09 16:33:19 +09:00
softsimon
70384d8d9f Merge pull request #5577 from mempool/natsoni/fix-premine-amount
Fix rune premine amount
2024-10-08 21:17:13 +09:00
natsoni
2a27ee0c7c Fix rune premine amount 2024-10-08 19:20:08 +09:00
nymkappa
933a204462 [accelerator] fee delta matches what the user accepted to pay in frontend 2024-10-08 18:37:03 +09:00
natsoni
6884830da6 Merge branch 'master' into natsoni/block-first-seen-audit 2024-10-08 15:04:46 +09:00
softsimon
24ec31acd9 Merge pull request #5569 from mempool/natsoni/ord
Add option to display runestones and inscriptions metadata
2024-10-08 13:21:43 +09:00
natsoni
1b2f1b38b4 undefined -> unknown 2024-10-08 13:09:19 +09:00
natsoni
3486c35f5e 50kb -> 100kb 2024-10-08 12:59:36 +09:00
natsoni
57a05c80a2 Move inscription type to utils 2024-10-08 12:53:18 +09:00
natsoni
1ddb8a39c9 Show text inscriptions up to 50kB 2024-10-08 12:50:56 +09:00
natsoni
0a61429176 Increase inscription max height 2024-10-08 12:41:14 +09:00
natsoni
e440c3f235 Fix edicts displaying 2024-10-08 12:40:25 +09:00
natsoni
177bbc83f3 Clean up etches fetching logic 2024-10-08 12:38:12 +09:00
Mononaut
040c067aac fix rune edict wrong id type bug 2024-10-08 02:49:46 +00:00
Mononaut
15b3c88a1f fix optional rune divisibility bug 2024-10-08 02:40:14 +00:00
natsoni
65f080d526 FIx error handling logic in ord-data 2024-10-08 11:24:17 +09:00
wiz
19347614bd Merge pull request #5514 from mempool/nymkappa/refactor-pool-subscription
[refactor] remove useless mining_pool subscriptions
2024-10-08 11:13:08 +09:00
softsimon
3b9601a82e Merge pull request #5575 from mempool/mononaut/minimal-runes
replace rune parsing libraries with minimal reimplementation
2024-10-08 11:10:54 +09:00
Mononaut
acae5a33b0 replace rune parsing dependencies with minimal reimplementation 2024-10-08 01:56:49 +00:00
natsoni
8b6db768cd Decode inscription / rune data client-side 2024-10-07 20:26:02 +09:00
natsoni
4143a5f593 Add runestone protocol implementation 2024-10-07 20:03:10 +09:00
natsoni
d31c2665ee Add inscriptions parsing code 2024-10-07 20:01:55 +09:00
softsimon
2142ae55d5 Merge pull request #5469 from mempool/nymkappa/configurable-pool-update
[mining] fix pools updater only running at start
2024-10-07 19:43:38 +09:00
softsimon
0c87a4e7f6 Merge branch 'master' into nymkappa/configurable-pool-update 2024-10-07 19:35:51 +09:00
softsimon
b08b2ce44a Merge pull request #5574 from mempool/nymkappa/update-doc
update doc
2024-10-07 15:44:00 +09:00
nymkappa
d6b9e3118d [refactor] remove useless mining_pool subscriptions 2024-10-07 15:41:29 +09:00
nymkappa
9b4c93c8ee update doc 2024-10-07 15:35:52 +09:00
wiz
e59f5b8810 Merge pull request #5565 from mempool/nymkappa/update-doc
[accelerator] public accel history filter by miner unique id
2024-10-07 15:17:55 +09:00
softsimon
ddf1a300b6 Merge pull request #5573 from mempool/mononaut/fix-partial-utxo-chart
never show a utxo chart with missing data
2024-10-07 15:16:25 +09:00
Mononaut
8e223861d6 never show a utxo chart with missing data 2024-10-06 20:25:49 +00:00
softsimon
8808ff1a98 Merge pull request #5572 from mempool/dependabot/npm_and_yarn/frontend/rollup-4.24.0
Bump rollup from 4.13.0 to 4.24.0 in /frontend
2024-10-06 14:56:35 +09:00
dependabot[bot]
33a6ba04b6 Bump rollup from 4.13.0 to 4.24.0 in /frontend
Bumps [rollup](https://github.com/rollup/rollup) from 4.13.0 to 4.24.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.13.0...v4.24.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-06 05:46:06 +00:00
softsimon
d020858840 Merge pull request #5549 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.15.0
Bump cypress from 13.14.0 to 13.15.0 in /frontend
2024-10-06 14:44:50 +09:00
softsimon
5e0160a039 Merge pull request #5571 from mempool/natsoni/fix-block-page-loadig
Fix block page spinner loader
2024-10-06 13:40:22 +09:00
softsimon
2443bebae5 Merge pull request #5570 from mempool/natsoni/failed-canceled
Failed -> Canceled
2024-10-06 13:38:49 +09:00
natsoni
6fb68203bc Fix block page spinner loader 2024-10-06 11:50:18 +09:00
natsoni
d7acfad3d6 Failed -> Canceled in accelerations list 2024-10-06 11:23:42 +09:00
softsimon
a700bd0ef1 Merge pull request #5568 from mempool/natsoni/gigasats
fix gigasats -> billion sats
2024-10-05 04:43:47 -06:00
natsoni
ae2a849257 fix gigasats -> billion sats 2024-10-05 19:34:41 +09:00
natsoni
1a75e3e317 Store block first seen time in block audits 2024-10-05 19:26:33 +09:00
softsimon
ba167c9cc2 Merge pull request #5566 from mempool/natsoni/fix-block-health-display
Avoid briefly showing incorrect health value
2024-10-05 02:38:11 -06:00
natsoni
3d27b7e7b4 Avoid briefly showing incorrect health value 2024-10-05 17:16:00 +09:00
nymkappa
c4f73b80da [accelerator] public accel history filter by miner unique id 2024-10-05 16:19:19 +09:00
softsimon
76a1eb12a6 Merge pull request #5563 from mempool/natsoni/fix-block-health-display
Reset block audit on block navigation
2024-10-04 22:29:14 -06:00
natsoni
fe16f0dddc Reset block audit on block navigation 2024-10-05 13:19:05 +09:00
natsoni
67295c1b9b add debug.log path to backend config 2024-10-04 22:15:00 +09:00
wiz
0bd760d4d6 Merge pull request #5561 from mempool/nymkappa/fix-tests
fix tests
2024-10-03 16:11:07 +09:00
nymkappa
0f2340600c fix tests 2024-10-03 15:48:27 +09:00
softsimon
72c9d02f88 Merge pull request #5558 from mempool/mononaut/handle-utxo-error
handle /utxos error on address page
2024-10-01 12:01:55 -07:00
wiz
43a42d356d Enable RUST_GBT in backend by default 2024-10-02 04:00:33 +09:00
Mononaut
60adad8db3 handle /utxos error on address page 2024-09-29 09:30:09 +00:00
softsimon
5b73362e44 Merge pull request #5557 from mempool/mononaut/fix-effective-rate-bug
Don't clobber effective fee rates
2024-09-28 21:24:07 +04:00
Mononaut
517a37728c Don't clobber effective fee rates 2024-09-28 16:34:13 +00:00
softsimon
8876bb8f43 Merge pull request #5556 from mempool/simon/remove-rocket-beta
remove rocket beta
2024-09-28 10:50:15 +04:00
softsimon
da2341dd00 remove rocket beta 2024-09-28 08:56:29 +04:00
softsimon
146935efaf Merge pull request #5553 from mempool/mononaut/cors-expose-custom-header
expose custom x-total-count header
2024-09-28 01:39:37 +04:00
softsimon
775fcbab31 Merge pull request #5552 from mempool/nymkappa/satoshi-pipe-update
export bitcoinsatoshis pipe module, allow custom class for first part
2024-09-28 00:04:56 +04:00
softsimon
cb12e66a3b Merge pull request #5554 from mempool/mononaut/fix-accel-paging
fix acceleration history paging w/ undefined total
2024-09-28 00:02:55 +04:00
Mononaut
ea08c0c950 fix acceleration history paging w/ undefined total 2024-09-27 16:09:12 +00:00
Mononaut
b26d26b14c expose custom x-total-count header 2024-09-27 15:55:29 +00:00
nymkappa
2d7316942f export bitcoinsatoshis pipe module, allow custom class for first part 2024-09-27 17:26:27 +02:00
wiz
676abf58fd Merge pull request #5551 from mempool/mononaut/utxo-chart-navigation
fix utxo chart on-click navigation
2024-09-27 07:50:15 +09:00
Mononaut
1d5843a112 fix utxo chart on-click navigation 2024-09-26 22:14:44 +00:00
softsimon
9bfe1fb15e Merge pull request #5550 from mempool/mononaut/truncate-miner-name
refactor miner name truncation
2024-09-26 23:59:12 +04:00
Mononaut
b29c4cf228 refactor miner name truncation 2024-09-26 17:18:49 +00:00
softsimon
1f84e1722f Merge pull request #5539 from BitcoinMechanic/add-miner-name
Show miner name on block timeline
2024-09-26 21:11:18 +04:00
dependabot[bot]
2ad52e2c78 Bump cypress from 13.14.0 to 13.15.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.14.0 to 13.15.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.14.0...v13.15.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-09-26 02:37:00 +00:00
softsimon
758122db5e Merge pull request #5548 from mempool/mononaut/utxo-chart-optimization
utxo chart optimization
2024-09-25 11:50:36 +08:00
Mononaut
83b6094174 optimize utxo graph layout algorithm, enable transitions 2024-09-25 00:03:15 +00:00
Stephan Oeste
7057b31c3c Add testnet4 to install script 2024-09-24 20:04:33 +02:00
Mononaut
9091fc9210 add missing time.service.ts file 2024-09-24 15:55:23 +00:00
mononaut
d149c8bd24 Merge branch 'master' into mononaut/utxo-chart-colors 2024-09-24 09:39:06 -06:00
Mononaut
9984621e5e refactor static time formatting into new service 2024-09-24 15:37:55 +00:00
softsimon
54a27ef89f Merge pull request #5545 from mempool/natsoni/fix-negative-time
Don't show negative timespans on timeline
2024-09-24 17:07:43 +08:00
mononaut
81ddce27df Merge branch 'master' into add-miner-name 2024-09-23 16:07:20 -06:00
BitcoinMechanic
e6dbde952e Strip non-alphanumeric chars from miner names 2024-09-23 12:36:10 -07:00
natsoni
2a9346f695 Don't show negative timespans on timeline 2024-09-23 14:47:57 +02:00
softsimon
05e88a25be npm audit fix 2024-09-23 14:15:00 +08:00
softsimon
574a800520 Merge pull request #5542 from mempool/dependabot/npm_and_yarn/frontend/esbuild-0.24.0
Bump esbuild from 0.23.0 to 0.24.0 in /frontend
2024-09-23 13:44:14 +08:00
softsimon
92de208414 Merge branch 'master' into mononaut/utxo-chart-colors 2024-09-23 13:01:32 +08:00
softsimon
1b4bbc24ba Merge pull request #5541 from mempool/mononaut/update-pool-pie
Update accelerating pie chart in real time
2024-09-23 12:57:07 +08:00
dependabot[bot]
0e5698955f Bump esbuild from 0.23.0 to 0.24.0 in /frontend
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.23.0 to 0.24.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.23.0...v0.24.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-09-23 02:09:59 +00:00
BitcoinMechanic
4220f99477 remove 'on'/UI changes per feedback 2024-09-22 14:46:53 -07:00
Mononaut
e144e139b7 Update accelerating pie chart in real time 2024-09-22 18:06:55 +00:00
Mononaut
06e699e52b address utxo chart color by age & updates 2024-09-22 17:09:35 +00:00
softsimon
07a0850f99 Merge pull request #5437 from mempool/simon/use-sats
only use sats, not sat
2024-09-23 00:33:19 +08:00
softsimon
72a5f4a521 amount selector sat -> sats 2024-09-23 00:18:59 +08:00
softsimon
04605e10a5 only use sats, not sat 2024-09-23 00:15:12 +08:00
softsimon
407ce3c76d Merge pull request #5491 from mempool/nymkappa/accel-bidboost-graph
[accelerator] make bid boost graph bar min height taller
2024-09-22 23:48:41 +08:00
softsimon
a99278320b Merge pull request #5451 from mempool/mononaut/optimize-gbt-process-blocks
optimize processNewBlocks
2024-09-22 23:46:29 +08:00
softsimon
367ee68fe0 Merge branch 'master' into mononaut/optimize-gbt-process-blocks 2024-09-22 12:41:51 +08:00
softsimon
1038b4f908 Merge pull request #5538 from mempool/natsoni/fix-wrong-block-skeleton
Fix race condition between accelerations and block audit api calls
2024-09-22 12:39:19 +08:00
BitcoinMechanic
b90cd4c7e3 restore minerNames property on pool 2024-09-20 14:59:21 -07:00
BitcoinMechanic
25482b9a06 show miner name on block timeline 2024-09-20 14:31:31 -07:00
natsoni
e41829d5e0 Fix race condition between accelerations and block audit api calls 2024-09-20 16:24:33 +02:00
softsimon
156bf12034 Merge pull request #5521 from mempool/dependabot/npm_and_yarn/backend/multi-d66d039ac5
Bump serve-static and express in /backend
2024-09-20 16:12:48 +08:00
softsimon
fc1cdbac22 Merge pull request #5522 from mempool/dependabot/npm_and_yarn/frontend/multi-9423f4c335
Bump body-parser and express in /frontend
2024-09-20 16:05:32 +08:00
softsimon
5c839aced3 Merge pull request #5486 from mempool/natsoni/remove-da-offset
Remove difficulty adjustment block offset
2024-09-20 15:37:14 +08:00
softsimon
3345a60863 Merge pull request #5532 from mempool/natsoni/fix-accelerations-list
Fix accelerations list page navigation on first load
2024-09-20 01:11:40 +08:00
mononaut
36844f5b70 Merge pull request #5533 from mempool/natsoni/add-blocks-logo
Add logos to blocks and test transactions pages
2024-09-19 10:44:53 -06:00
mononaut
46d99db167 Merge branch 'master' into natsoni/add-blocks-logo 2024-09-19 10:44:38 -06:00
softsimon
76dcb0830a Merge pull request #5529 from mempool/natsoni/footer-tm
"Be your own explorer" on non official mempool instance
2024-09-19 23:13:41 +08:00
natsoni
0b29b61e93 "Be your own explorer" on non official mempool instance 2024-09-19 17:07:31 +02:00
softsimon
f4425ed7fe Merge pull request #5524 from mempool/simon/calculator-numeric-fix
Fix critical calculator inputmode
2024-09-19 22:53:24 +08:00
softsimon
7c02eab630 Merge pull request #5525 from mempool/mononaut/utxo-chart
Add utxo chart to address page
2024-09-19 22:51:49 +08:00
softsimon
8867ef9680 Merge pull request #5534 from mempool/natsoni/mining-1m
Only fetch 1m mining stats
2024-09-19 22:50:29 +08:00
softsimon
1048a0ea83 Merge pull request #5536 from mempool/natsoni/fix-timeline-tooltip-width
Wrap pool logos in timeline tooltip
2024-09-19 22:46:40 +08:00
softsimon
7b216f7ec7 Merge pull request #5535 from mempool/natsoni/fix-eta-error
Fix ETA calculation error
2024-09-19 22:41:25 +08:00
natsoni
32cc2f0c63 Blocks logo wraps before 'Test Transactions' text 2024-09-19 16:33:31 +02:00
natsoni
c6b0e5ff0e Acceleration timeline tooltip: wrap pool logos 2024-09-19 16:08:39 +02:00
natsoni
68a580466f Fix ETA calculation error 2024-09-19 15:17:24 +02:00
natsoni
bb06a66a03 Only fetch 1m mining stats 2024-09-19 14:59:34 +02:00
natsoni
ec6372464f Add logos to blocks and test transactions pages 2024-09-19 14:38:37 +02:00
softsimon
c13b8029d3 Merge pull request #5527 from mempool/natsoni/hide-eta-replaced-tx
Pizza tracker: don't show ETA on replaced tx
2024-09-19 19:10:35 +08:00
softsimon
88e92b1b34 Merge pull request #5513 from mempool/nymkappa/fix-double-click
[accelerator] avoid duplicated accel request with double click
2024-09-19 17:46:15 +08:00
softsimon
b0fa3efbbb Merge pull request #5526 from mempool/natsoni/fix-mobile-tx-push
Fix mobile routing to tx push and test pages
2024-09-19 17:44:33 +08:00
softsimon
0ccb5618f6 Merge pull request #5530 from mempool/mononaut/fix-acc-eta
Fix off-by-one error in multi-pool eta calculation
2024-09-19 01:28:43 +08:00
natsoni
556313a676 Fix accelerations list page navigation on fist load 2024-09-18 16:56:00 +02:00
softsimon
1fe08a9ecc Merge pull request #5528 from mempool/natsoni/tracker-support-error
Show http error in pizza tracker
2024-09-18 22:33:48 +08:00
softsimon
a11116ff3a Merge pull request #5531 from mempool/natsoni/accelerator-logo-width
Fix accelerator logo in trademark policy on mobile
2024-09-18 22:33:18 +08:00
natsoni
ede0ccfd2e Fix accelerator logo width in trademark policy 2024-09-18 15:38:36 +02:00
Mononaut
b64caf8f4b Fix off-by-one error in multi-pool eta calculation 2024-09-17 20:30:25 +00:00
natsoni
99290a7946 Show http error in pizza tracker 2024-09-17 15:08:03 +02:00
natsoni
2d9709a427 Pizza tracker: hide ETA on replaced tx 2024-09-17 14:56:25 +02:00
natsoni
a76d6c2949 Fix mobile routing to tx push and test pages 2024-09-17 14:50:44 +02:00
softsimon
d8cfc6e32d Merge pull request #5471 from mempool/natsoni/hide-acc-checkout-on-accelerations
Avoid brief display of accelerator checkout on already accelerated txs
2024-09-15 16:07:58 +08:00
softsimon
9457032ab1 Merge branch 'master' into natsoni/hide-acc-checkout-on-accelerations 2024-09-14 23:12:43 +08:00
softsimon
74998e7f56 Merge pull request #5482 from mempool/natsoni/hide-acc-panel-on-acceleration
Hide accelerator panel if tx gets accelerated on another session
2024-09-14 20:15:39 +08:00
softsimon
db0f968749 Merge pull request #5481 from mempool/natsoni/fix-acceleration-flow-state
Reset acceleration flow state when leaving transaction
2024-09-14 17:52:50 +08:00
Mononaut
a1968e01e5 Add utxo chart to address page 2024-09-13 18:16:29 +00:00
softsimon
c7ab6b03fb Fix critical calculator inputmode 2024-09-13 23:23:22 +08:00
softsimon
2b206a7bcc Merge branch 'master' into natsoni/hide-acc-checkout-on-accelerations 2024-09-13 23:02:23 +08:00
softsimon
c4b90c2a18 Merge pull request #5485 from mempool/mononaut/paginated-accel-history
Handle paginated acceleration results
2024-09-13 22:56:57 +08:00
softsimon
2a7d5760e0 Merge branch 'master' into mononaut/paginated-accel-history 2024-09-13 22:16:28 +08:00
softsimon
c8719f1f1e Merge pull request #5479 from mempool/mononaut/rbf-tracking-fixes
RBF tracking fixes
2024-09-13 22:11:27 +08:00
natsoni
d199c7746e Merge branch 'master' into natsoni/hide-acc-checkout-on-accelerations 2024-09-13 11:57:20 +02:00
dependabot[bot]
67eb815992 Bump body-parser and express in /frontend
Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `body-parser` from 1.20.2 to 1.20.3
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-13 09:10:42 +00:00
dependabot[bot]
4ccd3c8525 Bump serve-static and express in /backend
Bumps [serve-static](https://github.com/expressjs/serve-static) to 1.16.2 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `serve-static` from 1.15.0 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-13 09:10:33 +00:00
softsimon
fc5af24b68 Merge branch 'master' into mononaut/rbf-tracking-fixes 2024-09-13 17:10:12 +08:00
softsimon
b17b66a52f Merge pull request #5476 from mempool/dependabot/npm_and_yarn/frontend/micromatch-4.0.8
Bump micromatch from 4.0.4 to 4.0.8 in /frontend
2024-09-13 17:09:53 +08:00
softsimon
819dedbc88 Merge pull request #5464 from mempool/natsoni/amount-selector
Add amount mode selector to footer
2024-09-13 17:03:24 +08:00
softsimon
8717051a06 Merge pull request #5472 from mempool/mononaut/json-errors
respect json Accept header in API error responses
2024-09-13 16:13:48 +08:00
nymkappa
3e78b636d6 [accelerator] avoid duplicated accel request with double click 2024-09-12 16:02:11 +02:00
nymkappa
b3ca8840e5 Merge branch 'master' into nymkappa/faucet-unverified 2024-09-11 16:51:22 +02:00
softsimon
75316e60d0 Merge branch 'master' into mononaut/json-errors 2024-09-10 18:37:27 +04:00
softsimon
31469ad361 Merge pull request #5478 from mempool/natsoni/ineligible-for-acceleration
Ineligible transaction link to accelerator FAQ
2024-09-10 18:36:19 +04:00
nymkappa
a133ddf062 [faucet] show unverified warning if no email provided 2024-09-10 12:07:46 +02:00
wiz
485a58e453 Merge pull request #5503 from mempool/mononaut/axios-socket-hotfix-2
hotfix option 2 for axios breaking change to unix sockets
2024-09-09 16:54:28 +09:00
wiz
92090399cc Merge pull request #5504 from mempool/revert-5502-mononaut/axios-socket-hotfix-1
Revert "hotfix option 1 for axios breaking change to unix sockets"
2024-09-09 16:54:12 +09:00
wiz
893c3cd87d Revert "hotfix option 1 for axios breaking change to unix sockets" 2024-09-09 16:53:56 +09:00
wiz
c93159414c Merge pull request #5502 from mempool/mononaut/axios-socket-hotfix-1
hotfix option 1 for axios breaking change to unix sockets
2024-09-09 16:42:23 +09:00
Mononaut
b2d4f4078f alternate hotfix for broken socket support (rollback axios to 1.7.2) 2024-09-08 20:18:04 +00:00
Mononaut
be17e45785 hotfix for axios breaking change to unix sockets 2024-09-08 20:16:06 +00:00
wiz
dbe774cc64 ops: Clear all mempool frontend configs on build env reset 2024-09-09 02:45:16 +09:00
wiz
64223c4744 ops: Set blocksxor=0 in bitcoin.conf 2024-09-05 02:15:08 +09:00
wiz
07fd3d3409 ops: Bump some FreeBSD install packages 2024-09-04 22:26:08 +09:00
wiz
f7360433a1 Merge pull request #5497 from mempool/nymkappa/nofaucet-error-message
[faucet] add missing error message for suspicious twitter accounts
2024-09-04 21:06:47 +09:00
nymkappa
f6fac92180 [faucet] add missing error message for suspicious twitter accounts 2024-09-04 13:52:48 +02:00
wiz
82d1502bfa Merge pull request #5487 from mempool/orangeusurf/about-enterprise-update
Update about page enterprise sponsors
2024-09-03 13:40:06 +09:00
orangesurf
8ab104d191 switch to alternate logo 2024-09-02 13:47:41 +02:00
orangesurf
263742132c Merge branch 'master' into orangeusurf/about-enterprise-update 2024-09-02 12:28:52 +01:00
softsimon
e3c3f31ddb Merge pull request #5494 from vostrnad/zero-multisig
Allow OP_0 in multisig scripts
2024-09-01 10:49:27 +04:00
wiz
70d1f52268 Merge pull request #5489 from mempool/mononaut/the-v3-standard 2024-09-01 02:02:21 +09:00
Vojtěch Strnad
e44f30d7a7 Allow OP_0 in multisig scripts 2024-08-31 14:31:55 +02:00
Mononaut
099d84a395 New standardness rules for v3 & anchor outputs, with activation height logic 2024-08-30 23:16:33 +00:00
Mononaut
12285465d9 Add support for anchor output type 2024-08-30 21:39:22 +00:00
natsoni
eab008c707 Ineligible transaction link to accelerator FAQ 2024-08-30 12:19:58 +02:00
nymkappa
0f1def5822 [accelerator] make bid boost graph bar min height taller 2024-08-29 20:53:40 +02:00
orangesurf
fad39e0bea Update about page enterprise sponsors 2024-08-29 13:02:32 +02:00
natsoni
0a5a2c3c7e Remove difficulty epoch block offset 2024-08-28 16:50:00 +02:00
Mononaut
b526ee0877 Handle paginated acceleration results 2024-08-28 14:47:42 +00:00
dependabot[bot]
98d98b2478 Bump micromatch from 4.0.4 to 4.0.8 in /frontend
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.4 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.4...4.0.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 05:01:52 +00:00
softsimon
3bea10ea35 Merge pull request #5484 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.14.0
Bump cypress from 13.13.0 to 13.14.0 in /frontend
2024-08-28 07:00:55 +02:00
dependabot[bot]
1ea45e9e96 Bump cypress from 13.13.0 to 13.14.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.13.0 to 13.14.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.13.0...v13.14.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-08-28 02:59:35 +00:00
natsoni
555425d97e Handle city-states in geolocation component 2024-08-27 14:49:54 +02:00
natsoni
624b3473fc Hide accelerator panel if tx gets accelerated on another session 2024-08-27 12:32:21 +02:00
natsoni
a3e61525fe Reset acceleration flow state when leaving transaction 2024-08-27 11:42:13 +02:00
Mononaut
9e05060af4 fix tests 2024-08-27 00:17:17 +00:00
Mononaut
ee53597fe9 Resume RBF trees after restart 2024-08-26 23:43:29 +00:00
Mononaut
e362003746 Catch RBF replacements across mempool update boundaries 2024-08-26 23:43:15 +00:00
Mononaut
185eae00e9 Fix RBF tracking inconsistencies 2024-08-26 23:41:38 +00:00
softsimon
8c2d0e1d6c Merge pull request #5445 from mempool/mononaut/persistent-goggles
Persist mempool block visualization between pages
2024-08-26 22:14:17 +02:00
softsimon
009fba3dd5 Merge branch 'master' into mononaut/persistent-goggles 2024-08-26 22:05:16 +02:00
softsimon
a0fc4861d4 Merge pull request #5460 from mempool/mononaut/v1-audit-improvements
v1 audit improvements
2024-08-26 21:38:37 +02:00
softsimon
62085581dd Merge branch 'master' into mononaut/v1-audit-improvements 2024-08-26 18:45:00 +02:00
softsimon
05efa8c300 Merge pull request #5468 from mempool/dependabot/npm_and_yarn/frontend/elliptic-6.5.7
Bump elliptic from 6.5.4 to 6.5.7 in /frontend
2024-08-26 17:59:21 +02:00
softsimon
eee99a6407 Merge pull request #5477 from mempool/mononaut/readme-node-version
[docs] update READMEs to newer node version
2024-08-26 17:58:52 +02:00
Mononaut
98cee4a6cd [docs] update READMEs to newer node version 2024-08-26 15:36:53 +00:00
dependabot[bot]
0302999806 Bump elliptic from 6.5.4 to 6.5.7 in /frontend
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.4 to 6.5.7.
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.4...v6.5.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 14:54:51 +00:00
softsimon
1876d67e74 Merge pull request #5467 from mempool/dependabot/npm_and_yarn/backend/axios-1.7.4
Bump axios from 1.7.2 to 1.7.4 in /backend
2024-08-26 16:53:40 +02:00
softsimon
c0bb75e5b1 Merge pull request #5475 from mempool/dependabot/npm_and_yarn/frontend/tslib-2.7.0
Bump tslib from 2.6.2 to 2.7.0 in /frontend
2024-08-26 16:53:26 +02:00
dependabot[bot]
4059a902a1 Bump tslib from 2.6.2 to 2.7.0 in /frontend
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.6.2 to 2.7.0.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/v2.6.2...v2.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 12:42:46 +00:00
dependabot[bot]
4cc19a7235 Bump axios from 1.7.2 to 1.7.4 in /backend
Bumps [axios](https://github.com/axios/axios) from 1.7.2 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.7.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 12:42:42 +00:00
wiz
c874d642c5 Bump version to 3.1.0-dev 2024-08-26 21:41:26 +09:00
wiz
f0af1703da Release v3.0.0 2024-08-24 18:35:41 +09:00
Mononaut
b47e148677 respect json Accept header in API error responses 2024-08-22 19:51:28 +00:00
natsoni
d22743c4b8 Don't display accelerator checkout on already accelerated txs 2024-08-22 15:39:20 +02:00
nymkappa
6db4afe878 [mining] add POOLS_UPDATE_DELAY where needed 2024-08-20 14:31:07 +02:00
nymkappa
4596394100 [mining] pool updater is now self contained service 2024-08-20 12:07:20 +02:00
nymkappa
ae2ed8fdae [mining] fix pools updater only running at start 2024-08-20 11:53:48 +02: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
natsoni
e59308c2f5 Fix global footer css 2024-08-19 18:21:01 +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
natsoni
c7f48b4390 Add amount mode selector to footer 2024-08-19 16:29:34 +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
c9171224e1 DB migration to fix bad v1 audits 2024-08-18 02:21:12 +00:00
Mononaut
248cef7718 Improve prioritized transaction detection algorithm 2024-08-18 02:21:05 +00:00
Mononaut
26c03eee88 update pool pie chart color scheme 2024-08-14 14:21:47 +00:00
Mononaut
a31729b8b8 fix feeDelta display logic 2024-08-10 21:56:11 +00:00
Mononaut
fbf27560b3 optimize processNewBlocks 2024-08-10 13:53:49 +00:00
Mononaut
79e494150c fix mined acceleration detection logic on tx pages 2024-08-09 14:44:51 +00:00
Mononaut
104c7f4285 Persist mempool block visualization between pages 2024-08-08 13:12:31 +00:00
175 changed files with 6595 additions and 2210 deletions

12
LICENSE
View File

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

View File

@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build
_Make sure to use Node.js 16.10 and npm 7._
_Make sure to use Node.js 20.x and npm 9.x or newer_
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._

View File

@@ -27,8 +27,9 @@
"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",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": false,
"RUST_GBT": false,
"RUST_GBT": true,
"LIMIT_GBT": false,
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6,
@@ -45,7 +46,8 @@
"PASSWORD": "mempool",
"TIMEOUT": 60000,
"COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
"COOKIE_PATH": "/path/to/bitcoin/.cookie",
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
},
"ELECTRUM": {
"HOST": "127.0.0.1",

View File

@@ -1,21 +1,22 @@
{
"name": "mempool-backend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-backend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"hasInstallScript": true,
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.7.2",
"axios": "1.7.2",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.19.2",
"express": "~4.21.1",
"maxmind": "~4.3.11",
"mysql2": "~3.11.0",
"redis": "^4.7.0",
@@ -2280,6 +2281,7 @@
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -2488,9 +2490,9 @@
}
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@@ -2500,7 +2502,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -2825,9 +2827,9 @@
"dev": true
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": {
"node": ">= 0.6"
}
@@ -3029,9 +3031,9 @@
"dev": true
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": {
"node": ">= 0.8"
}
@@ -3459,36 +3461,36 @@
}
},
"node_modules/express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.2",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -3601,12 +3603,12 @@
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@@ -6050,9 +6052,12 @@
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
@@ -6266,9 +6271,12 @@
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -6436,9 +6444,9 @@
"dev": true
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -6646,11 +6654,11 @@
]
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dependencies": {
"side-channel": "^1.0.4"
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
@@ -6871,9 +6879,9 @@
}
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -6906,6 +6914,14 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6917,14 +6933,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dependencies": {
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -9603,9 +9619,9 @@
}
},
"body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@@ -9615,7 +9631,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -9849,9 +9865,9 @@
"dev": true
},
"cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
},
"cookie-signature": {
"version": "1.0.6",
@@ -9996,9 +10012,9 @@
"dev": true
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
},
"error-ex": {
"version": "1.3.2",
@@ -10303,36 +10319,36 @@
}
},
"express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.2",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -10434,12 +10450,12 @@
}
},
"finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@@ -12236,9 +12252,9 @@
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
},
"merge-stream": {
"version": "2.0.0",
@@ -12401,9 +12417,9 @@
}
},
"object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
},
"on-finished": {
"version": "2.4.1",
@@ -12520,9 +12536,9 @@
"dev": true
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"path-type": {
"version": "4.0.0",
@@ -12664,11 +12680,11 @@
"dev": true
},
"qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"requires": {
"side-channel": "^1.0.4"
"side-channel": "^1.0.6"
}
},
"queue-microtask": {
@@ -12802,9 +12818,9 @@
"dev": true
},
"send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"requires": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -12836,6 +12852,11 @@
}
}
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -12849,14 +12870,14 @@
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"requires": {
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
"send": "0.19.0"
}
},
"set-function-length": {

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -42,10 +42,10 @@
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.7.2",
"axios": "1.7.2",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.19.2",
"express": "~4.21.1",
"maxmind": "~4.3.11",
"mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt",

View File

@@ -28,6 +28,7 @@
"INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": true,
"RUST_GBT": false,
"LIMIT_GBT": false,
@@ -46,7 +47,8 @@
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000,
"COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",

View File

@@ -1,5 +1,5 @@
import { Common } from '../../api/common';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
const randomTransactions = require('./test-data/transactions-random.json');
const replacedTransactions = require('./test-data/transactions-replaced.json');
@@ -10,14 +10,14 @@ describe('Common', () => {
describe('RBF', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
test('should detect RBF transactions with fast method', () => {
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = 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);
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');

View File

@@ -41,8 +41,9 @@ describe('Mempool Backend Config', () => {
STDOUT_LOG_MIN_PRIORITY: 'debug',
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
POOLS_UPDATE_DELAY: 604800,
AUDIT: false,
RUST_GBT: false,
RUST_GBT: true,
LIMIT_GBT: false,
CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0,
@@ -73,7 +74,8 @@ describe('Mempool Backend Config', () => {
PASSWORD: 'mempool',
TIMEOUT: 60000,
COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie'
COOKIE_PATH: '/bitcoin/.cookie',
DEBUG_LOG_PATH: '',
});
expect(config.SECOND_CORE_RPC).toStrictEqual({

View File

@@ -2,6 +2,7 @@ import config from '../config';
import logger from '../logger';
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import rbfCache from './rbf-cache';
import transactionUtils from './transaction-utils';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
@@ -15,7 +16,8 @@ class Audit {
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
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
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
let deprioritized: string[] = []; // lower 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
@@ -133,23 +135,7 @@ 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;
}
}
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
// transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);

View File

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

View File

@@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult {
},
['reject-reason']?: string,
}
export interface SubmitPackageResult {
package_msg: string;
"tx-results": { [wtxid: string]: TxResult };
"replaced-transactions"?: string[];
}
export interface TxResult {
txid: string;
"other-wtxid"?: string;
vsize?: number;
fees?: {
base: number;
"effective-feerate"?: number;
"effective-includes"?: string[];
};
error?: string;
}

View File

@@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import mempool from '../mempool';
@@ -196,6 +196,10 @@ class BitcoinApi implements AbstractBitcoinApi {
}
}
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult> {
return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined);
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return {
@@ -323,6 +327,7 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return'
};

View File

@@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
class BitcoinRoutes {
public initRoutes(app: Application) {
@@ -47,6 +48,8 @@ class BitcoinRoutes {
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
;
if (config.MEMPOOL.BACKEND !== 'esplora') {
@@ -86,7 +89,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -105,13 +108,13 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks();
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) {
res.status(500).send('Not an array');
handleError(req, res, 500, 'Not an array');
return;
}
const txIds: string[] = [];
@@ -128,12 +131,12 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') {
res.status(500).send('Invalid txids format');
handleError(req, res, 500, 'Invalid txids format');
return;
}
const txids = txids_csv.split(',');
if (txids.length > 50) {
res.status(400).send('Too many txids requested');
handleError(req, res, 400, 'Too many txids requested');
return;
}
@@ -141,13 +144,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async $getCpfpInfo(req: Request, res: Response) {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
res.status(501).send(`Invalid transaction ID.`);
handleError(req, res, 501, `Invalid transaction ID.`);
return;
}
@@ -180,7 +183,7 @@ class BitcoinRoutes {
try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) {
res.status(500).send('failed to get CPFP info');
handleError(req, res, 500, 'failed to get CPFP info');
return;
}
}
@@ -209,7 +212,7 @@ class BitcoinRoutes {
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
}
}
@@ -223,7 +226,7 @@ class BitcoinRoutes {
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
}
}
@@ -284,13 +287,13 @@ class BitcoinRoutes {
// Not modified
// 422 Unprocessable Entity
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`);
handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
}
} catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
res.status(404).send(e.message);
handleError(req, res, 404, e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}
@@ -304,7 +307,7 @@ class BitcoinRoutes {
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
}
}
@@ -314,7 +317,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -336,7 +339,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -346,7 +349,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain');
res.send(blockHeader);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -357,10 +360,11 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
return res.status(404).send(`audit not available`);
handleError(req, res, 404, `audit not available`);
return;
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -371,7 +375,8 @@ class BitcoinRoutes {
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`);
handleError(req, res, 404, `transaction audit not available`);
return;
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
@@ -388,42 +393,49 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlocksByBulk(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
return res.status(404).send(`This API is only available for Bitcoin networks`);
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
return;
}
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
return;
}
if (!Common.indexingEnabled()) {
return res.status(404).send(`Indexing is required for this API`);
handleError(req, res, 404, `Indexing is required for this API`);
return;
}
const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) {
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
return;
}
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) {
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
return;
}
if (from > to) {
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
return;
}
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -458,10 +470,10 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlockTransactions(req: Request, res: Response) {
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@@ -483,7 +495,7 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -492,13 +504,13 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
@@ -507,15 +519,16 @@ class BitcoinRoutes {
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
return res.status(413).send(e instanceof Error ? e.message : e);
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
}
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
@@ -528,23 +541,23 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
res.status(413).send(e instanceof Error ? e.message : e);
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
}
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') {
res.status(405).send('Address summary lookups require mempool/electrs backend.');
handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
return;
}
}
private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
@@ -555,15 +568,16 @@ class BitcoinRoutes {
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
return res.status(413).send(e instanceof Error ? e.message : e);
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
}
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
@@ -578,16 +592,16 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
res.status(413).send(e instanceof Error ? e.message : e);
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
}
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') {
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
return;
}
}
@@ -597,7 +611,7 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -624,7 +638,7 @@ class BitcoinRoutes {
const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -632,12 +646,13 @@ class BitcoinRoutes {
try {
const result = blocks.getCurrentBlockHeight();
if (!result) {
return res.status(503).send(`Service Temporarily Unavailable`);
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'text/plain');
res.send(result.toString());
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -647,7 +662,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -657,7 +672,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'application/octet-stream');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -666,7 +681,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -675,7 +690,7 @@ class BitcoinRoutes {
const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -688,7 +703,7 @@ class BitcoinRoutes {
replaces
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -697,7 +712,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -706,7 +721,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -719,7 +734,7 @@ class BitcoinRoutes {
res.status(204).send();
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -728,7 +743,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -738,10 +753,10 @@ class BitcoinRoutes {
if (da) {
res.json(da);
} else {
res.status(503).send(`Service Temporarily Unavailable`);
handleError(req, res, 503, `Service Temporarily Unavailable`);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -752,7 +767,7 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult);
} catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@@ -764,7 +779,7 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult);
} catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@@ -776,8 +791,20 @@ class BitcoinRoutes {
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 })
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
private async $submitPackage(req: Request, res: Response) {
try {
const rawTxs = Common.getTransactionsFromRequest(req);
const maxfeerate = parseFloat(req.query.maxfeerate as string);
const maxburnamount = parseFloat(req.query.maxburnamount as string);
const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, e.message && e.code ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}

View File

@@ -5,7 +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';
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
interface FailoverHost {
host: string,
@@ -332,6 +332,10 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.');
}
$submitPackage(rawTransactions: string[]): Promise<SubmitPackageResult> {
throw new Error('Method not implemented.');
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
}

View File

@@ -34,6 +34,7 @@ import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -219,10 +220,10 @@ class Blocks {
};
}
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
return {
id: hash,
transactions: Common.classifyTransactions(transactions),
transactions: Common.classifyTransactions(transactions, height),
};
}
@@ -342,7 +343,12 @@ class Blocks {
id: pool.uniqueId,
name: pool.name,
slug: pool.slug,
minerNames: null,
};
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
}
extras.matchRate = null;
@@ -616,7 +622,7 @@ class Blocks {
// add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
@@ -653,7 +659,7 @@ class Blocks {
}
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx;
@@ -912,7 +918,7 @@ class Blocks {
}
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);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) {
@@ -1169,7 +1175,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = Common.getTransactionFlags(tx);
flags = Common.getTransactionFlags(tx, height);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -1188,7 +1194,7 @@ class Blocks {
} else {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(hash, txs);
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@@ -1324,7 +1330,7 @@ class Blocks {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
summaryVersion = 1;
} else {
// Call Core RPC

View File

@@ -10,7 +10,6 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@@ -80,8 +79,8 @@ export class Common {
return arr;
}
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
// For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
@@ -96,7 +95,7 @@ export class Common {
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
});
if (foundMatches?.length) {
matches[addedTx.txid] = [...new Set(foundMatches)];
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
}
});
} else {
@@ -124,7 +123,7 @@ export class Common {
foundMatches.add(deletedTx);
}
if (foundMatches.size) {
matches[addedTx.txid] = [...foundMatches];
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
}
}
}
@@ -139,17 +138,17 @@ export class Common {
const replaced: Set<MempoolTransactionExtended> = new Set();
for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[i];
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
const key = `${vin.txid}:${vin.vout}`;
const match = spendMap.get(key);
if (match && match.txid !== tx.txid) {
replaced.add(match);
// remove this tx from the spendMap
// prevents the same tx being replaced more than once
for (const replacedVin of match.vin) {
const key = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(key);
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(replacedKey);
}
}
const key = `${vin.txid}:${vin.vout}`;
spendMap.delete(key);
}
if (replaced.size) {
@@ -200,10 +199,13 @@ export class Common {
*
* returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
*
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
* For now, just pull out individual rules into versioned functions where necessary.
*/
static isNonStandard(tx: TransactionExtended): boolean {
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
// version
if (tx.version > TX_MAX_STANDARD_VERSION) {
if (this.isNonStandardVersion(tx, height)) {
return true;
}
@@ -250,6 +252,8 @@ export class Common {
}
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true;
} else if (this.isNonStandardAnchor(tx, height)) {
return true;
}
// TODO: bad-witness-nonstandard
}
@@ -335,6 +339,49 @@ export class Common {
return false;
}
// Individual versioned standardness rules
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2;
}
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
return false;
}
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
if (
height != null
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true;
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;
@@ -415,7 +462,7 @@ export class Common {
return flags;
}
static getTransactionFlags(tx: TransactionExtended): number {
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
@@ -548,7 +595,7 @@ export class Common {
if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey;
}
// fast but bad heuristic to detect possible coinjoins
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
@@ -564,17 +611,17 @@ export class Common {
flags |= TransactionFlags.batch_payout;
}
if (this.isNonStandard(tx)) {
if (this.isNonStandard(tx, height)) {
flags |= TransactionFlags.nonstandard;
}
return Number(flags);
}
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
let flags = 0;
try {
flags = Common.getTransactionFlags(tx);
flags = Common.getTransactionFlags(tx, height);
} catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -585,8 +632,8 @@ export class Common {
};
}
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
return txs.map(Common.classifyTransaction);
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
return txs.map(tx => Common.classifyTransaction(tx, height));
}
static stripTransaction(tx: TransactionExtended): TransactionStripped {

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 81;
private static currentVersion = 83;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -700,6 +700,16 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81);
}
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
await this.$fixBadV1AuditBlocks();
await this.updateToSchemaVersion(82);
}
if (databaseSchemaVersion < 83 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
await this.updateToSchemaVersion(83);
}
}
/**
@@ -1314,6 +1324,28 @@ class DatabaseMigration {
logger.warn(`Failed to migrate cpfp transaction data`);
}
}
private async $fixBadV1AuditBlocks(): Promise<void> {
const badBlocks = [
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
];
for (const hash of badBlocks) {
try {
await this.$executeQuery(`
UPDATE blocks_audits
SET prioritized_txs = '[]'
WHERE hash = '${hash}'
`, true);
} catch (e) {
continue;
}
}
}
}
export default new DatabaseMigration();

View File

@@ -257,6 +257,7 @@ class DiskCache {
trees: rbfData.rbf.trees,
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
});
}
} catch (e) {

View File

@@ -1,6 +1,7 @@
import config from '../../config';
import { Application, Request, Response } from 'express';
import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
class ChannelsRoutes {
constructor() { }
@@ -22,7 +23,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -38,7 +39,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -53,11 +54,11 @@ class ChannelsRoutes {
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) {
res.status(400).send('Invalid index');
handleError(req, res, 400, 'Invalid index');
return;
}
if (['open', 'active', 'closed'].includes(status) === false) {
res.status(400).send('Invalid status');
handleError(req, res, 400, 'Invalid status');
return;
}
@@ -69,14 +70,14 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString());
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try {
if (!Array.isArray(req.query.txId)) {
res.status(400).send('Not an array');
handleError(req, res, 400, 'Not an array');
return;
}
const txIds: string[] = [];
@@ -107,7 +108,7 @@ class ChannelsRoutes {
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -119,7 +120,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -132,7 +133,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}

View File

@@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import channelsApi from './channels.api';
import statisticsApi from './statistics.api';
import { handleError } from '../../utils/api';
class GeneralLightningRoutes {
constructor() { }
@@ -27,7 +29,7 @@ class GeneralLightningRoutes {
channels: channels,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -41,7 +43,7 @@ class GeneralLightningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -50,7 +52,7 @@ class GeneralLightningRoutes {
const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}

View File

@@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api';
import DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces';
import { handleError } from '../../utils/api';
class NodesRoutes {
constructor() { }
@@ -31,7 +32,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -181,13 +182,13 @@ class NodesRoutes {
}
} catch (e) {}
}
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -195,7 +196,7 @@ class NodesRoutes {
try {
const node = await nodesApi.$getNode(req.params.public_key);
if (!node) {
res.status(404).send('Node not found');
handleError(req, res, 404, 'Node not found');
return;
}
res.header('Pragma', 'public');
@@ -203,7 +204,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -215,7 +216,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -223,7 +224,7 @@ class NodesRoutes {
try {
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
if (!node) {
res.status(404).send('Node not found');
handleError(req, res, 404, 'Node not found');
return;
}
res.header('Pragma', 'public');
@@ -231,7 +232,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -247,7 +248,7 @@ class NodesRoutes {
topByChannels: topChannelsNodes,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -259,7 +260,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -271,7 +272,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -283,7 +284,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -295,7 +296,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -307,7 +308,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -322,7 +323,7 @@ class NodesRoutes {
);
if (country.length === 0) {
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`);
return;
}
@@ -335,7 +336,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -349,7 +350,7 @@ class NodesRoutes {
);
if (isp.length === 0) {
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`);
return;
}
@@ -362,7 +363,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -374,7 +375,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}

View File

@@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
import config from '../../config';
import elementsParser from './elements-parser';
import icons from './icons';
import { handleError } from '../../utils/api';
class LiquidRoutes {
public initRoutes(app: Application) {
@@ -42,7 +43,7 @@ class LiquidRoutes {
res.setHeader('content-length', result.length);
res.send(result);
} else {
res.status(404).send('Asset icon not found');
handleError(req, res, 404, 'Asset icon not found');
}
}
@@ -51,7 +52,7 @@ class LiquidRoutes {
if (result) {
res.json(result);
} else {
res.status(404).send('Asset icons not found');
handleError(req, res, 404, 'Asset icons not found');
}
}
@@ -82,7 +83,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -94,7 +95,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -106,7 +107,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -118,7 +119,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -130,7 +131,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -142,7 +143,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -154,7 +155,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -166,7 +167,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -178,7 +179,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -190,7 +191,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -202,7 +203,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -214,7 +215,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -226,7 +227,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -238,7 +239,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -250,7 +251,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}

View File

@@ -369,7 +369,7 @@ class MempoolBlocks {
const lastBlockIndex = blocks.length - 1;
let hasBlockStack = blocks.length >= 8;
let stackWeight;
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
if (hasBlockStack) {
if (blockWeights && blockWeights[7] !== null) {
stackWeight = blockWeights[7];
@@ -380,28 +380,36 @@ class MempoolBlocks {
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
}
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let ancestor: MempoolTransactionExtended
for (const cluster of clusters) {
for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid];
if (mempoolTx) {
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
// ugly micro-optimization to avoid allocating new arrays
ancestors.length = 0;
descendants.length = 0;
let matched = false;
cluster.forEach(txid => {
ancestor = mempool[txid];
if (txid === memberTxid) {
matched = true;
} else {
if (!mempool[txid]) {
if (!ancestor) {
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
return;
}
const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: (mempool[txid].adjustedVsize * 4),
fee: ancestor.fee,
weight: (ancestor.adjustedVsize * 4),
};
if (matched) {
descendants.push(relative);
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) {
mempoolTx.lastBoosted = ancestor.firstSeen;
}
} else {
ancestors.push(relative);
}
@@ -410,7 +418,20 @@ class MempoolBlocks {
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
// ugly micro-optimization to avoid allocating new arrays or objects
if (mempoolTx.ancestors) {
mempoolTx.ancestors.length = 0;
} else {
mempoolTx.ancestors = [];
}
if (mempoolTx.descendants) {
mempoolTx.descendants.length = 0;
} else {
mempoolTx.descendants = [];
}
mempoolTx.ancestors.push(...ancestors);
mempoolTx.descendants.push(...descendants);
mempoolTx.cpfpChecked = true;
}
}
}
@@ -420,7 +441,10 @@ class MempoolBlocks {
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended;
const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
let acceleration: Acceleration;
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
const block = blocks[blockIndex];
let totalSize = 0;
let totalVsize = 0;
let totalWeight = 0;
@@ -436,7 +460,8 @@ class MempoolBlocks {
}
}
for (const txid of block) {
for (let i = 0; i < block.length; i++) {
const txid = block[i];
if (txid) {
mempoolTx = mempool[txid];
// save position in projected blocks
@@ -445,30 +470,37 @@ class MempoolBlocks {
vsize: totalVsize + (mempoolTx.vsize / 2),
};
const acceleration = accelerations[txid];
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;
if (txid in accelerations) {
acceleration = accelerations[txid];
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;
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) {
mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
}
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
}
delete mempoolTx.acceleration;
}
// online calculation of stack-of-blocks fee stats
@@ -486,7 +518,7 @@ class MempoolBlocks {
}
}
}
return this.dataToMempoolBlocks(
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
block,
transactions,
totalSize,
@@ -494,7 +526,7 @@ class MempoolBlocks {
totalFees,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
);
});
};
if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);

View File

@@ -19,12 +19,13 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private mempoolCandidates: { [txid: string ]: boolean } = {};
private spendMap = new Map<string, MempoolTransactionExtended>();
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
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 }[] } = {};
@@ -74,12 +75,12 @@ class Mempool {
}
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn;
}
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
candidates?: GbtCandidates) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn;
}
@@ -362,12 +363,15 @@ class Mempool {
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
this.recentlyDeleted.unshift(deletedTransactions);
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
}
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
this.updateTimerProgress(timer, 'completed async mempool callback');
}
@@ -541,16 +545,7 @@ class Mempool {
}
}
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
// Store replaced transactions
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
}
}
}
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
for (const rbfTransaction in rbfTransactions) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions

View File

@@ -10,6 +10,7 @@ import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes {
public initRoutes(app: Application) {
@@ -53,12 +54,12 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Prices are not available on testnets.');
handleError(req, res, 400, 'Prices are not available on testnets.');
return;
}
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string;
let response;
if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
@@ -71,7 +72,7 @@ class MiningRoutes {
}
res.status(200).send(response);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -84,9 +85,9 @@ class MiningRoutes {
res.json(stats);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
handleError(req, res, 404, e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}
@@ -103,9 +104,9 @@ class MiningRoutes {
res.json(poolBlocks);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
handleError(req, res, 404, e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}
@@ -129,7 +130,7 @@ class MiningRoutes {
res.json(pools);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -143,7 +144,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -157,7 +158,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -172,9 +173,9 @@ class MiningRoutes {
res.json(hashrates);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
handleError(req, res, 404, e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}
@@ -182,7 +183,7 @@ class MiningRoutes {
private async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0;
try {
currentHashrate = await bitcoinClient.getNetworkHashPs();
currentHashrate = await bitcoinClient.getNetworkHashPs(1008);
currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
@@ -203,7 +204,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -217,7 +218,7 @@ class MiningRoutes {
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);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -235,7 +236,7 @@ class MiningRoutes {
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);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -249,7 +250,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -263,7 +264,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -281,7 +282,7 @@ class MiningRoutes {
weights: blockWeights
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -293,7 +294,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -317,7 +318,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -326,7 +327,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) {
res.status(204).send(`This block has not been audited.`);
handleError(req, res, 204, `This block has not been audited.`);
return;
}
@@ -335,7 +336,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -358,7 +359,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -371,7 +372,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -384,7 +385,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null');
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -394,12 +395,12 @@ class MiningRoutes {
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.');
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -409,13 +410,13 @@ class MiningRoutes {
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.');
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -425,12 +426,12 @@ class MiningRoutes {
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.');
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -440,12 +441,12 @@ class MiningRoutes {
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.');
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -455,12 +456,12 @@ class MiningRoutes {
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.');
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(accelerationApi.accelerations || []);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -472,7 +473,7 @@ class MiningRoutes {
accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send();
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}

View File

@@ -44,6 +44,22 @@ interface CacheEvent {
value?: any,
}
/**
* Singleton for tracking RBF trees
*
* Maintains a set of RBF trees, where each tree represents a sequence of
* consecutive RBF replacements.
*
* Trees are identified by the txid of the root transaction.
*
* To maintain consistency, the following invariants must be upheld:
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
* - Existence: X in treeMap => treeMap(X) in rbfTrees
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
*/
class RbfCache {
private replacedBy: Map<string, string> = new Map();
private replaces: Map<string, string[]> = new Map();
@@ -61,6 +77,10 @@ class RbfCache {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
}
/**
* Low level cache operations
*/
private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
@@ -92,6 +112,12 @@ class RbfCache {
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
}
/**
* Basic data structure operations
* must uphold tree invariants
*/
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
return;
@@ -114,6 +140,10 @@ class RbfCache {
if (!replacedTx.rbf) {
txFullRbf = true;
}
if (this.replacedBy.has(replacedTx.txid)) {
// should never happen
continue;
}
this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid);
@@ -140,18 +170,47 @@ class RbfCache {
}
}
newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid;
const newTree = {
tx: newTx,
time: newTime,
fullRbf: treeFullRbf,
replaces: replacedTrees
};
this.addTree(treeId, newTree);
this.updateTreeMap(treeId, newTree);
this.addTree(newTree.tx.txid, newTree);
this.updateTreeMap(newTree.tx.txid, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
/**
* Read-only public interface
*/
public has(txId: string): boolean {
return this.txs.has(txId);
}
@@ -232,32 +291,6 @@ class RbfCache {
return changes;
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
}
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
this.evictionCount++;
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
// is the transaction involved in a full rbf replacement?
public isFullRbf(txid: string): boolean {
const treeId = this.treeMap.get(txid);
@@ -271,6 +304,10 @@ class RbfCache {
return tree?.fullRbf;
}
/**
* Cache maintenance & utility functions
*/
private cleanup(): void {
const now = Date.now();
for (const txid of this.expiring.keys()) {
@@ -299,10 +336,6 @@ class RbfCache {
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache
this.replacedBy.delete(tx);
// if this is the id of a tree, remove that too
if (this.treeMap.get(tx) === tx) {
this.removeTree(tx);
}
this.remove(tx);
}
}
@@ -370,14 +403,21 @@ class RbfCache {
};
}
public async load({ txs, trees, expiring, mempool }): Promise<void> {
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
try {
txs.forEach(txEntry => {
this.txs.set(txEntry.value.txid, txEntry.value);
});
this.staleCount = 0;
for (const deflatedTree of trees) {
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
if (tree) {
this.addTree(tree.tx.txid, tree);
this.updateTreeMap(tree.tx.txid, tree);
if (tree.mined) {
this.evict(tree.tx.txid);
}
}
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
@@ -385,6 +425,31 @@ class RbfCache {
}
});
this.staleCount = 0;
// connect cached trees to current mempool transactions
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
for (const tree of this.rbfTrees.values()) {
const tx = this.getTx(tree.tx.txid);
if (!tx || tree.mined) {
continue;
}
for (const vin of tx.vin) {
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
if (conflict && conflict.txid !== tx.txid) {
if (!conflicts[conflict.txid]) {
conflicts[conflict.txid] = {
replacedBy: conflict,
replaces: new Set(),
};
}
conflicts[conflict.txid].replaces.add(tx);
}
}
}
for (const { replacedBy, replaces } of Object.values(conflicts)) {
this.add([...replaces.values()], replacedBy);
}
await this.checkTrees();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup();
@@ -426,6 +491,12 @@ class RbfCache {
return;
}
// if this tx is already in the cache, return early
if (this.treeMap.has(txid)) {
this.removeTree(deflated.key);
return;
}
// recursively reconstruct child trees
for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
@@ -457,10 +528,6 @@ class RbfCache {
fullRbf: treeInfo.fullRbf,
replaces,
};
this.treeMap.set(txid, root);
if (root === txid) {
this.addTree(root, tree);
}
return tree;
}
@@ -511,6 +578,7 @@ class RbfCache {
processTxs(txs);
}
// evict missing transactions
for (const txid of txids) {
if (!found[txid]) {
this.evict(txid, false);

View File

@@ -365,6 +365,7 @@ class RedisCache {
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
expiring: rbfExpirations,
mempool: memPool.getMempool(),
spendMap: memPool.getSpendMap(),
});
}

View File

@@ -121,6 +121,7 @@ class TransactionUtils {
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
order: this.txidToOrdering(transaction.txid),
vsize,
@@ -128,7 +129,7 @@ class TransactionUtils {
sigops,
feePerVsize: feePerVbytes,
adjustedFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: effectiveFeePerVsize,
});
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
@@ -338,6 +339,87 @@ class TransactionUtils {
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript];
}
// calculate the most parsimonious set of prioritizations given a list of block transactions
// (i.e. the most likely prioritizations and deprioritizations)
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
// find the longest increasing subsequence of transactions
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
// should be O(n log n)
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
if (X.length < 2) {
return { prioritized: [], deprioritized: [] };
}
const N = X.length;
const P: number[] = new Array(N);
const M: number[] = new Array(N + 1);
M[0] = -1; // undefined so can be set to any value
let L = 0;
for (let i = 0; i < N; i++) {
// Binary search for the smallest positive l ≤ L
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
let lo = 1;
let hi = L + 1;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
if (X[M[mid]].rate > X[i].rate) {
hi = mid;
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
lo = mid + 1;
}
}
// After searching, lo == hi is 1 greater than the
// length of the longest prefix of X[i]
const newL = lo;
// The predecessor of X[i] is the last index of
// the subsequence of length newL-1
P[i] = M[newL - 1];
M[newL] = i;
if (newL > L) {
// If we found a subsequence longer than any we've
// found yet, update L
L = newL;
}
}
// Reconstruct the longest increasing subsequence
// It consists of the values of X at the L indices:
// ..., P[P[M[L]]], P[M[L]], M[L]
const LIS: any[] = new Array(L);
let k = M[L];
for (let j = L - 1; j >= 0; j--) {
LIS[j] = X[k];
k = P[k];
}
const lisMap = new Map<string, number>();
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
const prioritized: string[] = [];
const deprioritized: string[] = [];
let lastRate = X[0].rate;
for (const tx of X) {
if (lisMap.has(tx.txid)) {
lastRate = tx.rate;
} else {
if (Math.abs(tx.rate - lastRate) < 0.1) {
// skip if the rate is almost the same as the previous transaction
} else if (tx.rate <= lastRate) {
prioritized.push(tx.txid);
} else {
deprioritized.push(tx.txid);
}
}
}
return { prioritized, deprioritized };
}
}
export default new TransactionUtils();

View File

@@ -3,7 +3,8 @@ import * as WebSocket from 'ws';
import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolDelta, MempoolDeltaTxids
MempoolDelta, MempoolDeltaTxids,
TransactionCompressed
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -16,6 +17,7 @@ import transactionUtils from './transaction-utils';
import rbfCache, { ReplacementInfo } from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api';
import BlocksRepository from '../repositories/BlocksRepository';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import Audit from './audit';
@@ -34,6 +36,7 @@ interface AddressTransactions {
}
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read';
// valid 'want' subscriptions
const wantable = [
@@ -315,6 +318,7 @@ class WebsocketHandler {
if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
client['track-mempool-blocks'] = undefined;
const index = parsedMessage['track-mempool-block'];
client['track-mempool-block'] = index;
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
@@ -324,7 +328,31 @@ class WebsocketHandler {
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
});
} else {
client['track-mempool-block'] = null;
client['track-mempool-block'] = undefined;
}
}
if (parsedMessage && parsedMessage['track-mempool-blocks'] !== undefined) {
if (parsedMessage['track-mempool-blocks'].length > 0) {
client['track-mempool-block'] = undefined;
const indices: number[] = [];
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
const updates: { index: number, sequence: number, blockTransactions: TransactionCompressed[] }[] = [];
for (const i of parsedMessage['track-mempool-blocks']) {
const index = parseInt(i);
if (Number.isInteger(index) && index >= 0) {
indices.push(index);
updates.push({
index: index,
sequence: this.mempoolSequence,
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
});
}
}
client['track-mempool-blocks'] = indices;
response['projected-block-transactions'] = JSON.stringify(updates);
} else {
client['track-mempool-blocks'] = undefined;
}
}
@@ -520,8 +548,17 @@ class WebsocketHandler {
}
}
/**
*
* @param newMempool
* @param mempoolSize
* @param newTransactions array of transactions added this mempool update.
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
* @param accelerationDelta
* @param candidates
*/
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
@@ -529,6 +566,8 @@ class WebsocketHandler {
this.printLogs();
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
let added = newTransactions;
let removed = deletedTransactions;
@@ -547,7 +586,7 @@ class WebsocketHandler {
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions);
@@ -578,7 +617,7 @@ class WebsocketHandler {
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid]) {
for (const replaced of rbfTransactions[tx.txid].replaced) {
replacedTransactions.push({ replaced: replaced.txid, by: tx });
}
}
@@ -897,6 +936,19 @@ class WebsocketHandler {
delta: mBlockDeltas[index],
});
}
} else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
const indices = client['track-mempool-blocks'];
const updates: string[] = [];
for (const index of indices) {
if (mBlockDeltas[index]) {
updates.push(getCachedResponse(`projected-block-transactions-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
}));
}
}
response['projected-block-transactions'] = '[' + updates.join(',') + ']';
}
if (client['track-rbf'] === 'all' && rbfReplacements) {
@@ -947,7 +999,7 @@ class WebsocketHandler {
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.handleRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
@@ -1017,6 +1069,14 @@ class WebsocketHandler {
}
}
if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) {
const firstSeen = getRecentFirstSeen(block.id);
if (firstSeen) {
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
block.extras.firstSeen = firstSeen;
}
}
const confirmedTxids: { [txid: string]: boolean } = {};
// Update mempool to remove transactions included in the new block
@@ -1285,6 +1345,27 @@ class WebsocketHandler {
});
}
}
} else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
const indices = client['track-mempool-blocks'];
const updates: string[] = [];
for (const index of indices) {
if (mBlockDeltas && mBlockDeltas[index] && mBlocksWithTransactions[index]?.transactions?.length) {
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
updates.push(getCachedResponse(`projected-block-transactions-full-${index}`, {
index: index,
sequence: this.mempoolSequence,
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
}));
} else {
updates.push(getCachedResponse(`projected-block-transactions-delta-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
}));
}
}
}
response['projected-block-transactions'] = '[' + updates.join(',') + ']';
}
if (client['track-mempool-txids']) {

View File

@@ -32,6 +32,7 @@ interface IConfig {
AUTOMATIC_POOLS_UPDATE: boolean;
POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string,
POOLS_UPDATE_DELAY: number,
AUDIT: boolean;
RUST_GBT: boolean;
LIMIT_GBT: boolean;
@@ -85,6 +86,7 @@ interface IConfig {
TIMEOUT: number;
COOKIE: boolean;
COOKIE_PATH: string;
DEBUG_LOG_PATH: string;
};
SECOND_CORE_RPC: {
HOST: string;
@@ -192,8 +194,9 @@ const defaults: IConfig = {
'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',
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
'AUDIT': false,
'RUST_GBT': false,
'RUST_GBT': true,
'LIMIT_GBT': false,
'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0,
@@ -225,7 +228,8 @@ const defaults: IConfig = {
'PASSWORD': 'mempool',
'TIMEOUT': 60000,
'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie'
'COOKIE_PATH': '/bitcoin/.cookie',
'DEBUG_LOG_PATH': '',
},
'SECOND_CORE_RPC': {
'HOST': '127.0.0.1',

View File

@@ -211,6 +211,8 @@ class Server {
}
});
}
poolsUpdater.$startService();
}
async runMainUpdateLoop(): Promise<void> {

View File

@@ -299,6 +299,7 @@ export interface BlockExtension {
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
name: string;
slug: string;
minerNames: string[] | null;
};
avgFee: number;
avgFeeRate: number;
@@ -319,6 +320,7 @@ export interface BlockExtension {
segwitTotalSize: number;
segwitTotalWeight: number;
header: string;
firstSeen: number | null;
utxoSetChange: number;
// Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null;

View File

@@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips';
import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
interface DatabaseBlock {
id: string;
@@ -56,6 +57,7 @@ interface DatabaseBlock {
utxoSetChange: number;
utxoSetSize: number;
totalInputAmt: number;
firstSeen: number;
}
const BLOCK_DB_FIELDS = `
@@ -98,7 +100,8 @@ const BLOCK_DB_FIELDS = `
blocks.header,
blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmt
blocks.total_input_amt AS totalInputAmt,
UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen
`;
class BlocksRepository {
@@ -1020,6 +1023,24 @@ class BlocksRepository {
}
}
/**
* Save block first seen time
*
* @param id
*/
public async $saveFirstSeenTime(id: string, firstSeen: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET first_seen = FROM_UNIXTIME(?)
WHERE hash = ?`,
[firstSeen, id]
);
} catch (e) {
logger.err(`Cannot update block first seen time. 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
@@ -1054,6 +1075,7 @@ class BlocksRepository {
id: dbBlk.poolId,
name: dbBlk.poolName,
slug: dbBlk.poolSlug,
minerNames: null,
};
extras.avgFee = dbBlk.avgFee;
extras.avgFeeRate = dbBlk.avgFeeRate;
@@ -1076,6 +1098,7 @@ class BlocksRepository {
extras.utxoSetSize = dbBlk.utxoSetSize;
extras.totalInputAmt = dbBlk.totalInputAmt;
extras.virtualSize = dbBlk.weight / 4.0;
extras.firstSeen = dbBlk.firstSeen;
// Re-org can happen after indexing so we need to always get the
// latest state from core
@@ -1106,7 +1129,7 @@ class BlocksRepository {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@@ -1123,6 +1146,10 @@ class BlocksRepository {
}
}
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
blk.extras = <BlockExtension>extras;
return <BlockExtended>blk;
}

View File

@@ -83,6 +83,7 @@ module.exports = {
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
stop: 'stop',
submitBlock: 'submitblock', // bitcoind v0.7.0+
submitPackage: 'submitpackage',
validateAddress: 'validateaddress',
verifyChain: 'verifychain', // bitcoind v0.9.0+
verifyMessage: 'verifymessage',

View File

@@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info';
import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https';
import { Common } from '../api/common';
/**
* Maintain the most recent version of pools-v2.json
*/
class PoolsUpdater {
tag = 'PoolsUpdater';
lastRun: number = 0;
currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
public async $startService(): Promise<void> {
while ('Bitcoin is still alive') {
try {
await this.updatePoolsJson();
} catch (e: any) {
logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag);
}
await Common.sleep$(10000);
}
}
public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false
@@ -23,11 +37,8 @@ class PoolsUpdater {
return;
}
const oneWeek = 604800;
const oneDay = 86400;
const now = new Date().getTime() / 1000;
if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart
if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart
return;
}
@@ -43,7 +54,7 @@ class PoolsUpdater {
this.currentSha = await this.getShaFromDb();
}
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag);
if (this.currentSha !== null && this.currentSha === githubSha) {
return;
}
@@ -53,16 +64,16 @@ class PoolsUpdater {
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_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`);
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag);
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`, this.tag);
return;
}
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === null) {
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag);
} else {
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag);
}
const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) {
@@ -71,7 +82,7 @@ class PoolsUpdater {
poolsParser.setMiningPools(poolsJson);
if (config.DATABASE.ENABLED === false) { // Don't run db operations
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`);
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag);
return;
}
@@ -81,14 +92,14 @@ class PoolsUpdater {
await this.updateDBSha(githubSha);
await DB.query('COMMIT;');
} catch (e) {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag);
await DB.query('ROLLBACK;');
}
logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
} catch (e) {
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
this.lastRun = now - 600; // Try again in 10 minutes
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag);
}
}
@@ -102,7 +113,7 @@ class PoolsUpdater {
await DB.query('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) {
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
}
}
}
@@ -115,7 +126,7 @@ class PoolsUpdater {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null);
} catch (e) {
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
return null;
}
}
@@ -134,7 +145,7 @@ class PoolsUpdater {
}
}
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag);
return null;
}
@@ -186,7 +197,7 @@ class PoolsUpdater {
}
return data.data;
} catch (e) {
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
retry++;
}
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);

9
backend/src/utils/api.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Request, Response } from 'express';
export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void {
if (req.accepts('json')) {
res.status(statusCode).json({ error: errorMessage });
} else {
res.status(statusCode).send(errorMessage);
}
}

View File

@@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opN) {
return;
}
if (!opN.startsWith('OP_PUSHNUM_')) {
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opM) {
return;
}
if (!opM.startsWith('OP_PUSHNUM_')) {
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
@@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number {
} else {
return 9;
}
}
/** Extracts miner names from a DATUM coinbase transaction */
export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null {
let bytes: number[] = [];
for (let c = 0; c < coinbaseRaw.length; c += 2) {
bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16));
}
// Skip block height
let tagLengthByte = 1 + bytes[0];
let tagsLength = bytes[tagLengthByte];
if (tagsLength == 0x4c) {
tagLengthByte += 1;
tagsLength = bytes[tagLengthByte];
}
const tagStart = tagLengthByte + 1;
const tags = bytes.slice(tagStart, tagStart + tagsLength);
let tagString = String.fromCharCode(...tags);
tagString = tagString.replace('\x00', '');
return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, ''));
}

View File

@@ -0,0 +1,58 @@
import * as fs from 'fs';
import logger from '../logger';
import config from '../config';
function readFile(filePath: string, bufferSize?: number): string[] {
const fileSize = fs.statSync(filePath).size;
const chunkSize = bufferSize || fileSize;
const fileDescriptor = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(chunkSize);
fs.readSync(fileDescriptor, buffer, 0, chunkSize, fileSize - chunkSize);
fs.closeSync(fileDescriptor);
const lines = buffer.toString('utf8', 0, chunkSize).split('\n');
return lines;
}
function extractDateFromLogLine(line: string): number | undefined {
// Extract time from log: "2021-08-31T12:34:56Z" or "2021-08-31T12:34:56.123456Z"
const dateMatch = line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{6})?Z/);
if (!dateMatch) {
return undefined;
}
const dateStr = dateMatch[0];
const date = new Date(dateStr);
let timestamp = Math.floor(date.getTime() / 1000); // Remove decimal (microseconds are added later)
const timePart = dateStr.split('T')[1];
const microseconds = timePart.split('.')[1] || '';
if (!microseconds) {
return timestamp;
}
return parseFloat(timestamp + '.' + microseconds);
}
export function getRecentFirstSeen(hash: string): number | undefined {
const debugLogPath = config.CORE_RPC.DEBUG_LOG_PATH;
if (debugLogPath) {
try {
// Read the last few lines of debug.log
const lines = readFile(debugLogPath, 2048);
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (line && line.includes(`Saw new header hash=${hash}`)) {
return extractDateFromLogLine(line);
}
}
} catch (e) {
logger.err(`Cannot parse block first seen time from Core logs. Reason: ` + (e instanceof Error ? e.message : e));
}
}
return undefined;
}

View File

@@ -109,6 +109,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"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",
"POOLS_UPDATE_DELAY": 604800,
"CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6,
@@ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
MEMPOOL_POOLS_JSON_URL: ""
MEMPOOL_POOLS_JSON_TREE_URL: ""
MEMPOOL_POOLS_UPDATE_DELAY: ""
MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""

View File

@@ -36,6 +36,7 @@
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
},
@@ -46,7 +47,8 @@
"PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__,
"COOKIE": __CORE_RPC_COOKIE__,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",

View File

@@ -29,8 +29,9 @@ __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__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_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
__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}
@@ -48,6 +49,7 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
# ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -187,6 +189,7 @@ sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORIT
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_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json
@@ -205,6 +208,7 @@ sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__CORE_RPC_DEBUG_LOG_PATH__!${__CORE_RPC_DEBUG_LOG_PATH__}!g" mempool-config.json
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json

View File

@@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
### 3. Run the Frontend
_Make sure to use Node.js 16.10 and npm 7._
_Make sure to use Node.js 20.x and npm 9.x or newer._
Install project dependencies and run the frontend server:
@@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
### 1. Build the Frontend
_Make sure to use Node.js 16.10 and npm 7._
_Make sure to use Node.js 20.x and npm 9.x or newer._
Build the frontend:

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -92,10 +92,10 @@
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.23.0",
"esbuild": "^0.24.0",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
"tslib": "~2.8.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
@@ -105,7 +105,7 @@
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0",
"browser-sync": "^3.0.0",
"browser-sync": "^3.0.3",
"http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
@@ -115,7 +115,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.13.0",
"cypress": "^13.15.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from './app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
import { EightMempoolComponent } from './components/eight-mempool/eight-mempool.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
@@ -205,6 +206,10 @@ let routes: Routes = [
path: 'view/blocks',
component: EightBlocksComponent,
},
{
path: 'view/mempool-blocks',
component: EightMempoolComponent,
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },

View File

@@ -6,6 +6,7 @@ import { ZONE_SERVICE } from './injection-tokens';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service';
import { OrdApiService } from './services/ord-api.service';
import { StateService } from './services/state.service';
import { CacheService } from './services/cache.service';
import { PriceService } from './services/price.service';
@@ -21,6 +22,7 @@ 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 { TimeService } from './services/time.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';
@@ -31,6 +33,7 @@ import { DatePipe } from '@angular/common';
const providers = [
ElectrsApiService,
OrdApiService,
StateService,
CacheService,
PriceService,
@@ -42,6 +45,7 @@ const providers = [
EnterpriseService,
LanguageService,
ThemeService,
TimeService,
ShortenStringPipe,
FiatShortenerPipe,
FiatCurrencyPipe,

View File

@@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
return;
}
const opN = ops.pop();
if (!opN.startsWith('OP_PUSHNUM_')) {
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
@@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
}
}
const opM = ops.pop();
if (!opM.startsWith('OP_PUSHNUM_')) {
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)[0], 10);

View File

@@ -53,7 +53,7 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image">
<defs>
<style>
.d {
@@ -125,17 +125,14 @@
<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">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
<a href="https://bitkey.world/" target="_blank" title="Bitkey">
<img class="image" src="/resources/profile/bitkey.svg" />
<span>Bitkey</span>
</a>
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
@@ -150,7 +147,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)"/>
@@ -191,12 +188,30 @@
</svg>
<span>Exodus</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://leather.io/" target="_blank" title="Leather">
<img class="image" src="/resources/profile/leather.svg" />
<span>Leather</span>
</a>
<a href="https://taprootwizards.com/" target="_blank" title="Taproot Wizards">
<img class="image" src="/resources/profile/wizardhat.png" />
<span>Taproot Wizards</span>
</a>
</div>
</div>
<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">
<div class="community-sponsor whale-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper">
<ng-container>
@@ -435,7 +450,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@@ -13,8 +13,6 @@
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro {
@@ -94,6 +92,13 @@
}
}
.whale-sponsor {
img {
width: 70px;
height: 70px;
}
}
.alliances {
margin-bottom: 100px;
a {
@@ -158,9 +163,8 @@
margin: 40px 29px 10px;
&.image.coldcard {
border-radius: 0;
width: auto;
max-height: 50px;
margin: 40px 29px 14px 29px;
height: auto;
margin: 20px 29px 20px;
}
}
}
@@ -254,3 +258,12 @@
width: 64px;
height: 64px;
}
.enterprise-sponsor {
.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
}
}

View File

@@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Output() changeMode = new EventEmitter<boolean>();
calculating = true;
processing = false;
selectedOption: 'wait' | 'accel';
cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data
@@ -196,9 +197,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (changes.scrollEvent && this.scrollEvent) {
this.scrollToElement('acceleratePreviewAnchor', 'start');
}
if (changes.accelerating) {
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success');
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
}
}
}
@@ -371,6 +374,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.validateChoice();
}
}
@@ -378,9 +382,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* Account-based acceleration request
*/
accelerateWithMempoolAccount(): void {
if (!this.canPay || this.calculating) {
if (!this.canPay || this.calculating || this.processing) {
return;
}
this.processing = true;
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
@@ -390,6 +395,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
@@ -397,6 +403,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.moveToStep('paid');
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
}
});
@@ -466,10 +473,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* APPLE PAY
*/
async requestApplePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@@ -494,6 +505,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
console.error(`Unable to find apple pay button id='apple-pay-button'`);
// Try again
setTimeout(this.requestApplePayPayment.bind(this), 500);
this.processing = false;
return;
}
this.loadingApplePay = false;
@@ -505,6 +517,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
@@ -513,9 +526,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID
this.accelerationUUID,
costUSD
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
@@ -526,6 +541,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
@@ -537,6 +553,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
@@ -547,6 +564,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
});
} catch (e) {
this.processing = false;
console.error(e);
}
}
@@ -557,10 +575,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* GOOGLE PAY
*/
async requestGooglePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@@ -595,6 +617,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
@@ -603,9 +626,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID
this.accelerationUUID,
costUSD
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
@@ -616,6 +641,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
@@ -627,6 +653,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
@@ -644,10 +671,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* CASHAPP
*/
async requestCashAppPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
@@ -678,6 +709,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.addEventListener('ontokenization', event => {
const { tokenResult, error } = event.detail;
if (error) {
this.processing = false;
this.accelerateError = error;
} else if (tokenResult.status === 'OK') {
this.servicesApiService.accelerateWithCashApp$(
@@ -685,9 +717,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
this.accelerationUUID
this.accelerationUUID,
costUSD
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.cashAppPay) {
@@ -702,6 +736,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {

View File

@@ -12,7 +12,7 @@
</p>
</div>
<div class="spacer"></div>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
<div class="spacer"></div>
<div class="spacer"></div>
</div>

View File

@@ -21,14 +21,14 @@
</tr>
<tr *ngIf="accelerationInfo.fee">
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
</tr>
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
@if (accelerationInfo.status === 'accelerated') {
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
} @else {
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
}
</tr>
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
@@ -47,13 +47,14 @@
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td class="value" *ngIf="accelerationInfo.pools">
<ng-container *ngFor="let pool of accelerationInfo.pools">
<ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
<img *ngIf="accelerationInfo.poolsData[pool]"
class="pool-logo"
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'"
[alt]="'Logo of ' + pool.name + ' mining pool'">
<br *ngIf="i % 6 === 5">
</ng-container>
</td>
</tr>

View File

@@ -23,6 +23,7 @@
.label {
padding-right: 30px;
vertical-align: top;
}
.pool-logo {
@@ -30,7 +31,8 @@
height: 22px;
position: relative;
top: -1px;
margin-right: 3px;
margin-right: 4px;
margin-bottom: 4px;
}
.oobFees {

View File

@@ -9,7 +9,7 @@
<div class="interval">
<div class="interval-time">
@if (eta) {
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
~<app-time [time]="eta?.wait / 1000"></app-time>
}
</div>
</div>
@@ -38,7 +38,7 @@
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
<app-time [time]="acceleratedAt - transactionTime"></app-time>
<app-time [time]="firstSeenToAccelerated"></app-time>
</div>
</div>
<div class="node-spacer"></div>
@@ -46,10 +46,8 @@
<div class="interval-time">
@if (tx.status.confirmed) {
<div class="interval-time">
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
<app-time [time]="acceleratedToMined"></app-time>
</div>
} @else if (standardETA && !tx.status.confirmed) {
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
}
</div>
</div>

View File

@@ -11,19 +11,16 @@ import { MiningService } from '../../services/mining.service';
})
export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;
@Input() acceleratedAt: number;
@Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() eta: ETA;
// A mined transaction has standard ETA and accelerated ETA undefined
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
@Input() standardETA: number;
@Input() acceleratedETA: number;
acceleratedAt: number;
now: number;
accelerateRatio: number;
useAbsoluteTime: boolean = false;
interval: number;
firstSeenToAccelerated: number;
acceleratedToMined: number;
tooltipPosition = null;
hoverInfo: any = null;
@@ -34,38 +31,24 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
) {}
ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
this.updateTimes();
this.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.poolsData[pool.unique_id] = pool;
}
});
this.interval = window.setInterval(() => {
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
}, 60000);
}
ngOnChanges(changes): void {
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
// if (changes?.eta?.currentValue) {
// if (changes?.acceleratedETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
// } else if (changes?.standardETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
// }
// }
// }
this.updateTimes();
}
ngOnDestroy(): void {
clearInterval(this.interval);
updateTimes(): void {
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
this.firstSeenToAccelerated = Math.max(0, this.acceleratedAt - this.transactionTime);
this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt);
}
onHover(event, status: string): void {

View File

@@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
type: 'bar',
barWidth: '90%',
large: true,
barMinHeight: 1,
barMinHeight: 3,
},
],
dataZoom: (this.widget || data.length === 0 )? undefined : [{

View File

@@ -33,7 +33,7 @@
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
</td>
<td class="bid text-right">
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sats">sats</span>
</td>
<td class="time text-right">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@@ -41,7 +41,7 @@
</ng-container>
<ng-container *ngIf="!pending">
<td *ngIf="acceleration.boost != null" class="fee text-right">
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sats">sats</span>
</td>
<td *ngIf="acceleration.boost == null" class="fee text-right">
~
@@ -64,7 +64,7 @@
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
@@ -61,8 +61,11 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
this.paramSubscription = this.route.params.pipe(
tap(params => {
this.paramSubscription = combineLatest([
this.route.params,
timer(0),
]).pipe(
tap(([params]) => {
this.page = +params['page'] || 1;
this.pageSubject.next(this.page);
})

View File

@@ -10,10 +10,10 @@
</td>
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) {
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate>
}
</div>
</td>

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '../../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
@@ -23,7 +23,8 @@ function toRGB({r,g,b}): string {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActiveAccelerationBox implements OnChanges {
@Input() tx: Transaction;
@Input() acceleratedBy?: number[];
@Input() effectiveFeeRate?: number;
@Input() accelerationInfo: Acceleration;
@Input() miningStats: MiningStats;
@Input() pools: number[];
@@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges {
timespan = '';
chartInstance: any = undefined;
constructor() {}
constructor(
private cd: ChangeDetectorRef,
) {}
ngOnChanges(changes: SimpleChanges): void {
const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy;
if (pools && this.miningStats) {
this.prepareChartOptions(pools);
}
@@ -67,13 +70,17 @@ export class ActiveAccelerationBox implements OnChanges {
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
const lightenStep = acceleratingPools.length ? (0.48 / acceleratingPools.length) : 0;
// Find the first pool with at least 1% of the total network hashrate
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
data.push(getDataItem(
pool.lastEstimatedHashrate,
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)),
index >= firstSignificantPool
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
: 'white',
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true,
) as PieSeriesOption);
@@ -128,6 +135,7 @@ export class ActiveAccelerationBox implements OnChanges {
}
]
};
this.cd.markForCheck();
}
onChartInit(ec) {

View File

@@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
}
handleVin() {
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]);
if (address?.scripts.size) {
const script = address?.scripts.values().next().value;
if (script.template?.label) {

View File

@@ -94,6 +94,20 @@
</div>
</ng-container>
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && utxos && utxos.length > 2">
<br>
<div class="title-tx">
<h2 class="text-left" i18n="address.unspent-outputs">Unspent Outputs</h2>
</div>
<div class="box">
<div class="row">
<div class="col-md">
<app-utxo-graph [utxos]="utxos" left="80" />
</div>
</div>
</div>
</ng-container>
<br>
<div class="title-tx">
<h2 class="text-left">

View File

@@ -2,12 +2,12 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface';
import { Address, ChainStats, Transaction, Utxo, Vin } from '../../interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs';
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
@@ -104,6 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
addressString: string;
isLoadingAddress = true;
transactions: Transaction[];
utxos: Utxo[];
isLoadingTransactions = true;
retryLoadMore = false;
error: any;
@@ -159,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.address = null;
this.isLoadingTransactions = true;
this.transactions = null;
this.utxos = null;
this.addressInfo = null;
this.exampleChannel = null;
document.body.scrollTo(0, 0);
@@ -212,11 +214,23 @@ export class AddressComponent implements OnInit, OnDestroy {
this.updateChainStats();
this.isLoadingAddress = false;
this.isLoadingTransactions = true;
return address.is_pubkey
const utxoCount = this.chainStats.utxos + this.mempoolStats.utxos;
return forkJoin([
address.is_pubkey
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
: this.electrsApiService.getAddressTransactions$(address.address);
: this.electrsApiService.getAddressTransactions$(address.address),
(utxoCount > 2 && utxoCount <= 500 ? (address.is_pubkey
? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
: this.electrsApiService.getAddressUtxos$(address.address)) : of(null)).pipe(
catchError(() => {
return of(null);
})
)
]);
}),
switchMap((transactions) => {
switchMap(([transactions, utxos]) => {
this.utxos = utxos;
this.tempTransactions = transactions;
if (transactions.length) {
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
@@ -309,6 +323,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions = this.transactions.slice();
this.mempoolStats.removeTx(transaction);
this.audioService.playSound('magic');
this.confirmTransaction(tx);
} else {
if (this.addTransaction(transaction, false)) {
this.audioService.playSound('magic');
@@ -334,6 +349,31 @@ export class AddressComponent implements OnInit, OnDestroy {
}
}
// update utxos in-place
if (this.utxos != null) {
let utxosChanged = false;
for (const vin of transaction.vin) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
if (utxoIndex !== -1) {
this.utxos.splice(utxoIndex, 1);
utxosChanged = true;
}
}
for (const [index, vout] of transaction.vout.entries()) {
if (vout.scriptpubkey_address === this.address.address) {
this.utxos.push({
txid: transaction.txid,
vout: index,
value: vout.value,
status: JSON.parse(JSON.stringify(transaction.status)),
});
utxosChanged = true;
}
}
if (utxosChanged) {
this.utxos = this.utxos.slice();
}
}
return true;
}
@@ -346,9 +386,65 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions.splice(index, 1);
this.transactions = this.transactions.slice();
// update utxos in-place
if (this.utxos != null) {
let utxosChanged = false;
for (const vin of transaction.vin) {
if (vin.prevout?.scriptpubkey_address === this.address.address) {
this.utxos.push({
txid: vin.txid,
vout: vin.vout,
value: vin.prevout.value,
status: { confirmed: true }, // Assuming the input was confirmed
});
utxosChanged = true;
}
}
for (const [index, vout] of transaction.vout.entries()) {
if (vout.scriptpubkey_address === this.address.address) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
if (utxoIndex !== -1) {
this.utxos.splice(utxoIndex, 1);
utxosChanged = true;
}
}
}
if (utxosChanged) {
this.utxos = this.utxos.slice();
}
}
return true;
}
confirmTransaction(transaction: Transaction): void {
// update utxos in-place
if (this.utxos != null) {
let utxosChanged = false;
for (const vin of transaction.vin) {
if (vin.prevout?.scriptpubkey_address === this.address.address) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
if (utxoIndex !== -1) {
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
utxosChanged = true;
}
}
}
for (const [index, vout] of transaction.vout.entries()) {
if (vout.scriptpubkey_address === this.address.address) {
const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
if (utxoIndex !== -1) {
this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status));
utxosChanged = true;
}
}
}
if (utxosChanged) {
this.utxos = this.utxos.slice();
}
}
}
loadMore(): void {
if (this.isLoadingTransactions || this.fullyLoaded) {
return;

View File

@@ -0,0 +1,7 @@
<div [formGroup]="amountForm" class="text-small text-center">
<select formControlName="mode" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 70px;" (change)="changeMode()">
<option value="btc" i18n="shared.btc|BTC">BTC</option>
<option value="sats" i18n="shared.sats">sats</option>
<option value="fiat" i18n="shared.fiat|Fiat">Fiat</option>
</select>
</div>

View File

@@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-amount-selector',
templateUrl: './amount-selector.component.html',
styleUrls: ['./amount-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AmountSelectorComponent implements OnInit {
amountForm: UntypedFormGroup;
modes = ['btc', 'sats', 'fiat'];
constructor(
private formBuilder: UntypedFormBuilder,
private stateService: StateService,
private storageService: StorageService,
) { }
ngOnInit() {
this.amountForm = this.formBuilder.group({
mode: ['btc']
});
this.stateService.viewAmountMode$.subscribe((mode) => {
this.amountForm.get('mode')?.setValue(mode);
});
}
changeMode() {
const newMode = this.amountForm.get('mode')?.value;
this.storageService.setValue('view-amount-mode', newMode);
this.stateService.viewAmountMode$.next(newMode);
}
}

View File

@@ -30,7 +30,7 @@
@if (digitsInfo === '1.8-8') {
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
} @else {
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : satoshis < 1000 && satoshis > -1000 ? 0 : 1 }}
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : (satoshis < 1000 && satoshis > -1000 ? 0 : 1) : undefined : true }}
}
<span class="symbol">
<ng-container *ngTemplateOutlet="prefix"></ng-container>sats

View File

@@ -198,7 +198,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
// initialize the scene without any entry transition
setup(transactions: TransactionStripped[]): void {
setup(transactions: TransactionStripped[], sort: boolean = false): void {
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags();
@@ -206,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.filtersAvailable = filtersAvailable;
if (this.scene) {
this.clearUpdateQueue();
this.scene.setup(transactions);
this.scene.setup(transactions, sort);
this.readyNextFrame = true;
this.start();
this.updateSearchHighlight();
@@ -553,7 +553,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
x: cssX,
y: cssY
};
const selected = this.scene.getTxAt({ x, y });
const selected = this.scene.getTxAt({ x, y: this.displayHeight - y });
const currentPreview = this.selectedTx || this.hoverTx;
if (selected !== currentPreview) {
@@ -627,7 +627,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (this.scene) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
const selected = this.scene.getTxAt({ x, y });
const selected = this.scene.getTxAt({ x, y: this.displayHeight - y });
if (selected && selected.txid) {
this.txClickEvent.emit({ tx: selected, keyModifier });
}
@@ -681,10 +681,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
// WebGL shader attributes
const attribs = {
offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
bounds: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
@@ -707,10 +706,9 @@ varying lowp vec4 vColor;
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
// shader interpolates between start and end values at the given rate, from the given time
attribute vec2 offset;
attribute vec4 bounds;
attribute vec4 posX;
attribute vec4 posY;
attribute vec4 posR;
attribute vec4 colR;
attribute vec4 colG;
attribute vec4 colB;
@@ -735,10 +733,7 @@ float interpolateAttribute(vec4 attr) {
void main() {
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
float radius = interpolateAttribute(posR);
vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
vec2 position = clamp(vec2(interpolateAttribute(posX), interpolateAttribute(posY)), bounds.xy, bounds.zw);
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
float red = interpolateAttribute(colR);

View File

@@ -18,6 +18,8 @@ export default class BlockScene {
animationOffset: number;
highlightingEnabled: boolean;
filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n;
x: number;
y: number;
width: number;
height: number;
gridWidth: number;
@@ -31,14 +33,16 @@ export default class BlockScene {
animateUntil = 0;
dirty: boolean;
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
constructor({ x = 0, y = 0, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
{ x?: number, y?: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
) {
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
this.init({ x, y,width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
}
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
resize({ x = 0, y = 0, width = this.width, height = this.height, animate = true }: { x?: number, y?: number, width?: number, height?: number, animate: boolean }): void {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.gridSize = this.width / this.gridWidth;
@@ -88,16 +92,19 @@ export default class BlockScene {
}
// set up the scene with an initial set of transactions, without any transition animation
setup(txs: TransactionStripped[]) {
setup(txs: TransactionStripped[], sort: boolean = false) {
// clean up any old transactions
Object.values(this.txs).forEach(tx => {
tx.destroy();
delete this.txs[tx.txid];
});
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
txs.forEach(tx => {
const txView = new TxView(tx, this);
this.txs[tx.txid] = txView;
let txViews = txs.map(tx => new TxView(tx, this));
if (sort) {
txViews = txViews.sort(feeRateDescending);
}
txViews.forEach(txView => {
this.txs[txView.txid] = txView;
this.place(txView);
this.saveGridToScreenPosition(txView);
this.applyTxUpdate(txView, {
@@ -221,7 +228,11 @@ export default class BlockScene {
getTxAt(position: Position): TxView | void {
if (this.layout) {
const gridPosition = this.screenToGrid(position);
return this.layout.getTx(gridPosition);
if (gridPosition.x >= 0 && gridPosition.x < this.gridWidth && gridPosition.y >= 0 && gridPosition.y < this.gridHeight) {
return this.layout.getTx(gridPosition);
} else {
return null;
}
} else {
return null;
}
@@ -235,8 +246,8 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
}
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
private init({ x, y, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
{ x: number, y: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
): void {
this.animationDuration = animationDuration || this.animationDuration || 1000;
@@ -261,7 +272,7 @@ export default class BlockScene {
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
this.gridWidth = resolution;
this.gridHeight = resolution;
this.resize({ width, height, animate: true });
this.resize({ x, y, width, height, animate: true });
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
this.txs = {};
@@ -271,7 +282,7 @@ export default class BlockScene {
}
private applyTxUpdate(tx: TxView, update: ViewUpdateParams): void {
this.animateUntil = Math.max(this.animateUntil, tx.update(update));
this.animateUntil = Math.max(this.animateUntil, tx.update(update, { minX: this.x, maxX: this.x + this.width, minY: this.y, maxY: this.y + this.height }));
}
private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void {
@@ -387,6 +398,7 @@ export default class BlockScene {
position: {
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
s: tx.screenPosition.s
}
},
duration: this.animationDuration,
@@ -446,18 +458,18 @@ export default class BlockScene {
break;
}
return {
x: x + this.unitPadding - (slotSize / 2),
y: y + this.unitPadding - (slotSize / 2),
x: this.x + x + this.unitPadding - (slotSize / 2),
y: this.y + y + this.unitPadding - (slotSize / 2),
s: squareSize
};
} else {
return { x: 0, y: 0, s: 0 };
return { x: this.x, y: this.y, s: 0 };
}
}
private screenToGrid(position: Position): Position {
let x = position.x;
let y = this.height - position.y;
let x = position.x - this.x;
let y = position.y - this.y;
let t;
switch (this.orientation) {

View File

@@ -2,12 +2,13 @@ import { FastVertexArray } from './fast-vertex-array';
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
const attribKeys = ['a', 'b', 't', 'v'];
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
const updateKeys = ['x', 'y', 'r', 'g', 'b', 'a'];
const attributeKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
export default class TxSprite {
static vertexSize = 30;
static vertexSize = 28;
static vertexCount = 6;
static dataSize: number = (30 * 6);
static dataSize: number = (28 * 6);
vertexArray: FastVertexArray;
vertexPointer: number;
@@ -16,15 +17,26 @@ export default class TxSprite {
attributes: Attributes;
tempAttributes: OptionalAttributes;
minX: number;
maxX: number;
minY: number;
maxY: number;
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray) {
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray, minX: number, maxX: number, minY: number, maxY: number) {
const offsetTime = params.start;
this.vertexArray = vertexArray;
this.vertexData = Array(VI.length).fill(0);
this.vertexData = Array(TxSprite.dataSize).fill(0);
this.updateMap = {
x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0
};
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
this.attributes = {
x: { a: params.x, b: params.x, t: offsetTime, v: 0, d: 0 },
y: { a: params.y, b: params.y, t: offsetTime, v: 0, d: 0 },
@@ -77,11 +89,24 @@ export default class TxSprite {
minDuration: minimum remaining transition duration when adjust = true
temp: if true, this update is only temporary (can be reversed with 'resume')
*/
update(params: SpriteUpdateParams): void {
update(params: SpriteUpdateParams, minX?: number, maxX?: number, minY?: number, maxY?: number): void {
const offsetTime = params.start || performance.now();
const v = params.duration > 0 ? (1 / params.duration) : 0;
updateKeys.forEach(key => {
if (minX != null) {
this.minX = minX;
}
if (maxX != null) {
this.maxX = maxX;
}
if (minY != null) {
this.minY = minY;
}
if (maxY != null) {
this.maxY = maxY;
}
attributeKeys.forEach(key => {
this.updateMap[key] = params[key];
});
@@ -139,18 +164,32 @@ export default class TxSprite {
...this.tempAttributes
};
}
const size = attributes.s;
// update vertex data in place
// ugly, but avoids overhead of allocating large temporary arrays
const vertexStride = VI.length + 2;
const vertexStride = VI.length + 4;
for (let vertex = 0; vertex < 6; vertex++) {
this.vertexData[vertex * vertexStride] = vertexOffsetFactors[vertex][0];
this.vertexData[(vertex * vertexStride) + 1] = vertexOffsetFactors[vertex][1];
for (let step = 0; step < VI.length; step++) {
this.vertexData[vertex * vertexStride] = this.minX;
this.vertexData[(vertex * vertexStride) + 1] = this.minY;
this.vertexData[(vertex * vertexStride) + 2] = this.maxX;
this.vertexData[(vertex * vertexStride) + 3] = this.maxY;
// x
this.vertexData[(vertex * vertexStride) + 4] = attributes[VI[0].a][VI[0].f] + (vertexOffsetFactors[vertex][0] * attributes.s.a);
this.vertexData[(vertex * vertexStride) + 5] = attributes[VI[1].a][VI[1].f] + (vertexOffsetFactors[vertex][0] * attributes.s.b);
this.vertexData[(vertex * vertexStride) + 6] = attributes[VI[2].a][VI[2].f];
this.vertexData[(vertex * vertexStride) + 7] = attributes[VI[3].a][VI[3].f];
// y
this.vertexData[(vertex * vertexStride) + 8] = attributes[VI[4].a][VI[4].f] + (vertexOffsetFactors[vertex][1] * attributes.s.a);
this.vertexData[(vertex * vertexStride) + 9] = attributes[VI[5].a][VI[5].f] + (vertexOffsetFactors[vertex][1] * attributes.s.b);
this.vertexData[(vertex * vertexStride) + 10] = attributes[VI[6].a][VI[6].f];
this.vertexData[(vertex * vertexStride) + 11] = attributes[VI[7].a][VI[7].f];
for (let step = 8; step < VI.length; step++) {
// components of each field in the vertex array are defined by an entry in VI:
// VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors
this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f];
this.vertexData[(vertex * vertexStride) + step + 4] = attributes[VI[step].a][VI[step].f];
}
}

View File

@@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
flags: number;
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
scene?: BlockScene;
@@ -106,7 +106,7 @@ export default class TxView implements TransactionStripped {
returns minimum transition end time
*/
update(params: ViewUpdateParams): number {
update(params: ViewUpdateParams, { minX, maxX, minY, maxY }: { minX: number, maxX: number, minY: number, maxY: number }): number {
if (params.jitter) {
params.delay += (Math.random() * params.jitter);
}
@@ -115,21 +115,35 @@ export default class TxView implements TransactionStripped {
this.initialised = true;
this.sprite = new TxSprite(
toSpriteUpdate(params),
this.vertexArray
this.vertexArray,
minX,
maxX,
minY,
maxY
);
// apply any pending hover event
if (this.hover) {
params.duration = Math.max(params.duration, hoverTransitionTime);
this.sprite.update({
...this.hoverColor,
duration: hoverTransitionTime,
adjust: false,
temp: true
});
this.sprite.update(
{
...this.hoverColor,
duration: hoverTransitionTime,
adjust: false,
temp: true
},
minX,
maxX,
minY,
maxY
);
}
} else {
this.sprite.update(
toSpriteUpdate(params)
toSpriteUpdate(params),
minX,
maxX,
minY,
maxY
);
}
this.dirty = false;

View File

@@ -11,6 +11,10 @@ export function hexToColor(hex: string): Color {
};
}
export function colorToHex(color: Color): string {
return [color.r, color.g, color.b].map(c => Math.round(c * 255).toString(16)).join('');
}
export function desaturate(color: Color, amount: number): Color {
const gray = (color.r + color.g + color.b) / 6;
return {
@@ -30,6 +34,15 @@ export function darken(color: Color, amount: number): Color {
};
}
export function mix(color1: Color, color2: Color, amount: number): Color {
return {
r: color1.r * (1 - amount) + color2.r * amount,
g: color1.g * (1 - amount) + color2.g * amount,
b: color1.b * (1 - amount) + color2.b * amount,
a: color1.a * (1 - amount) + color2.a * amount,
};
}
export function setOpacity(color: Color, opacity: number): Color {
return {
...color,
@@ -142,6 +155,10 @@ export function defaultColorFunction(
return auditColors.added_prioritized;
case 'prioritized':
return auditColors.prioritized;
case 'added_deprioritized':
return auditColors.added_prioritized;
case 'deprioritized':
return auditColors.prioritized;
case 'selected':
return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1];
case 'accelerated':

View File

@@ -0,0 +1,24 @@
<div class="block-overview-graph">
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
@if (!disableSpinner) {
<div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable">
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div>
}
<app-block-overview-tooltip
[tx]="selectedTx || hoverTx"
[cursorPosition]="tooltipPosition"
[clickable]="!!selectedTx"
[auditEnabled]="auditHighlighting"
[blockConversion]="blockConversion"
[filterFlags]="activeFilterFlags"
[filterMode]="filterMode"
[relativeTime]="relativeTime"
></app-block-overview-tooltip>
<app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
<div *ngIf="!webGlEnabled" class="placeholder">
<span i18n="webgl-disabled">Your browser does not support this feature.</span>
</div>
</div>

View File

@@ -0,0 +1,67 @@
.block-overview-graph {
position: relative;
width: 100%;
height: 100%;
background: var(--stat-box-bg);
display: flex;
justify-content: center;
align-items: center;
grid-column: 1/-1;
.placeholder {
display: flex;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
}
}
.graph-alignment {
position: relative;
width: 100%;
}
.grid-align {
display: grid;
grid-template-columns: repeat(auto-fit, 75px);
justify-content: center;
}
.block-overview-canvas {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
overflow: hidden;
&.clickable {
cursor: pointer;
}
}
.loader-wrapper {
position: absolute;
background: #181b2d7f;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
transition: opacity 500ms 500ms;
pointer-events: none;
&.hidden {
opacity: 0;
}
}

View File

@@ -0,0 +1,803 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { FastVertexArray } from '../block-overview-graph/fast-vertex-array';
import BlockScene from '../block-overview-graph/block-scene';
import TxSprite from '../block-overview-graph/tx-sprite';
import TxView from '../block-overview-graph/tx-view';
import { Color, Position } from '../block-overview-graph/sprite-types';
import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { ThemeService } from '../../services/theme.service';
import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '../block-overview-graph/utils';
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils';
const unmatchedOpacity = 0.2;
const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
};
const unmatchedContrastAuditColors = {
censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity),
missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity),
added: setOpacity(contrastAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity),
};
@Component({
selector: 'app-block-overview-multi',
templateUrl: './block-overview-multi.component.html',
styleUrls: ['./block-overview-multi.component.scss'],
})
export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, OnChanges {
@Input() isLoading: boolean;
@Input() resolution: number;
@Input() numBlocks: number;
@Input() padding: number = 0;
@Input() blockWidth: number = 360;
@Input() autofit: boolean = false;
@Input() blockLimit: number;
@Input() orientation = 'left';
@Input() flip = true;
@Input() animationDuration: number = 1000;
@Input() animationOffset: number | null = null;
@Input() disableSpinner = false;
@Input() mirrorTxid: string | void;
@Input() unavailable: boolean = false;
@Input() auditHighlighting: boolean = false;
@Input() showFilters: boolean = false;
@Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null;
@Input() filterMode: FilterMode = 'and';
@Input() gradientMode: 'fee' | 'age' = 'fee';
@Input() relativeTime: number | null;
@Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@Output() txHoverEvent = new EventEmitter<string>();
@Output() readyEvent = new EventEmitter();
@ViewChild('blockCanvas')
canvas: ElementRef<HTMLCanvasElement>;
themeChangedSubscription: Subscription;
gl: WebGLRenderingContext;
animationFrameRequest: number;
animationHeartBeat: number;
displayWidth: number;
displayHeight: number;
displayBlockWidth: number;
displayPadding: number;
cssWidth: number;
cssHeight: number;
shaderProgram: WebGLProgram;
vertexArray: FastVertexArray;
running: boolean;
scenes: BlockScene[] = [];
hoverTx: TxView | void;
selectedTx: TxView | void;
highlightTx: TxView | void;
mirrorTx: TxView | void;
tooltipPosition: Position;
readyNextFrame = false;
lastUpdate: number = 0;
pendingUpdates: {
count: number,
add: { [txid: string]: TransactionStripped },
remove: { [txid: string]: string },
change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } },
direction?: string,
}[] = [];
searchText: string;
searchSubscription: Subscription;
filtersAvailable: boolean = true;
activeFilterFlags: bigint | null = null;
webGlEnabled = true;
constructor(
readonly ngZone: NgZone,
readonly elRef: ElementRef,
public stateService: StateService,
private themeService: ThemeService,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
}
ngAfterViewInit(): void {
if (this.canvas) {
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
this.initScenes();
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
this.themeChangedSubscription = this.themeService.themeChanged$.subscribe(() => {
for (const scene of this.scenes) {
scene.setColorFunction(this.getColorFunction());
}
});
}
}
}
initScenes(): void {
for (const scene of this.scenes) {
if (scene) {
scene.destroy();
}
}
this.scenes = [];
this.pendingUpdates = [];
for (let i = 0; i < this.numBlocks; i++) {
this.scenes.push(null);
this.pendingUpdates.push({
count: 0,
add: {},
remove: {},
change: {},
direction: 'left',
});
}
this.resizeCanvas();
this.start();
}
ngOnChanges(changes): void {
if (changes.numBlocks) {
this.initScenes();
}
if (changes.orientation || changes.flip) {
for (const scene of this.scenes) {
scene?.setOrientation(this.orientation, this.flip);
}
}
if (changes.auditHighlighting) {
this.setHighlightingEnabled(this.auditHighlighting);
}
if (changes.overrideColor) {
for (const scene of this.scenes) {
scene?.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
}
}
if ((changes.filterFlags || changes.showFilters || changes.filterMode || changes.gradientMode)) {
this.setFilterFlags();
}
}
setFilterFlags(goggle?: ActiveFilter): void {
this.filterMode = goggle?.mode || this.filterMode;
this.gradientMode = goggle?.gradient || 'fee'; // this.gradientMode;
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
for (const scene of this.scenes) {
if (scene) {
if (this.activeFilterFlags != null && this.filtersAvailable) {
scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode));
} else {
scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
}
}
}
this.start();
}
ngOnDestroy(): void {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
clearTimeout(this.animationHeartBeat);
}
if (this.canvas) {
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
this.themeChangedSubscription?.unsubscribe();
}
}
clear(block: number, direction): void {
this.exit(block, direction);
this.start();
}
destroy(block: number): void {
if (this.scenes[block]) {
this.scenes[block].destroy();
this.clearUpdateQueue(block);
this.start();
}
}
// initialize the scene without any entry transition
setup(block: number, transactions: TransactionStripped[], sort: boolean = false): void {
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags();
}
this.filtersAvailable = filtersAvailable;
if (this.scenes[block]) {
this.clearUpdateQueue(block);
this.scenes[block].setup(transactions, sort);
this.readyNextFrame = true;
this.start();
}
}
enter(block: number, transactions: TransactionStripped[], direction: string): void {
if (this.scenes[block]) {
this.clearUpdateQueue(block);
this.scenes[block].enter(transactions, direction);
this.start();
}
}
exit(block: number, direction: string): void {
if (this.scenes[block]) {
this.clearUpdateQueue(block);
this.scenes[block].exit(direction);
this.start();
}
}
replace(block: number, transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
if (this.scenes[block]) {
this.clearUpdateQueue(block);
this.scenes[block].replace(transactions || [], direction, sort, startTime);
this.start();
}
}
// collates deferred updates into a set of consistent pending changes
queueUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
for (const tx of add) {
this.pendingUpdates[block].add[tx.txid] = tx;
delete this.pendingUpdates[block].remove[tx.txid];
delete this.pendingUpdates[block].change[tx.txid];
}
for (const txid of remove) {
delete this.pendingUpdates[block].add[txid];
this.pendingUpdates[block].remove[txid] = txid;
delete this.pendingUpdates[block].change[txid];
}
for (const tx of change) {
if (this.pendingUpdates[block].add[tx.txid]) {
this.pendingUpdates[block].add[tx.txid].rate = tx.rate;
this.pendingUpdates[block].add[tx.txid].acc = tx.acc;
} else {
this.pendingUpdates[block].change[tx.txid] = tx;
}
}
this.pendingUpdates[block].direction = direction;
this.pendingUpdates[block].count++;
}
deferredUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
this.queueUpdate(block, add, remove, change, direction);
this.applyQueuedUpdates();
}
applyQueuedUpdates(): void {
for (const [index, pendingUpdate] of this.pendingUpdates.entries()) {
if (pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
this.applyUpdate(index, Object.values(pendingUpdate.add), Object.values(pendingUpdate.remove), Object.values(pendingUpdate.change), pendingUpdate.direction);
this.clearUpdateQueue(index);
}
}
}
clearUpdateQueue(block: number): void {
this.pendingUpdates[block] = {
count: 0,
add: {},
remove: {},
change: {},
};
this.lastUpdate = performance.now();
}
update(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
// merge any pending changes into this update
this.queueUpdate(block, add, remove, change, direction);
this.applyUpdate(block,Object.values(this.pendingUpdates[block].add), Object.values(this.pendingUpdates[block].remove), Object.values(this.pendingUpdates[block].change), direction, resetLayout);
this.clearUpdateQueue(block);
}
applyUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scenes[block]) {
add = add.filter(tx => !this.scenes[block].txs[tx.txid]);
remove = remove.filter(txid => this.scenes[block].txs[txid]);
change = change.filter(tx => this.scenes[block].txs[tx.txid]);
if (this.gradientMode === 'age') {
this.scenes[block].updateAllColors();
}
this.scenes[block].update(add, remove, change, direction, resetLayout);
this.start();
this.lastUpdate = performance.now();
}
}
initCanvas(): void {
if (!this.canvas || !this.gl) {
return;
}
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
const shaderSet = [
{
type: this.gl.VERTEX_SHADER,
src: vertShaderSrc
},
{
type: this.gl.FRAGMENT_SHADER,
src: fragShaderSrc
}
];
this.shaderProgram = this.buildShaderProgram(shaderSet);
this.gl.useProgram(this.shaderProgram);
// Set up alpha blending
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
const glBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
/* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => {
attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
this.gl.enableVertexAttribArray(attribs[key].pointer);
});
this.start();
}
handleContextLost(event): void {
event.preventDefault();
cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null;
this.running = false;
this.gl = null;
}
handleContextRestored(event): void {
if (this.canvas?.nativeElement) {
this.gl = this.canvas.nativeElement.getContext('webgl');
if (this.gl) {
this.initCanvas();
}
}
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
if (this.canvas) {
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
this.displayWidth = window.devicePixelRatio * this.cssWidth;
this.displayHeight = window.devicePixelRatio * this.cssHeight;
this.displayBlockWidth = window.devicePixelRatio * this.blockWidth;
this.displayPadding = window.devicePixelRatio * this.padding;
this.canvas.nativeElement.width = this.displayWidth;
this.canvas.nativeElement.height = this.displayHeight;
if (this.gl) {
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
}
for (let i = 0; i < this.scenes.length; i++) {
const blocksPerRow = Math.floor(this.displayWidth / (this.displayBlockWidth + (this.displayPadding * 2)));
const x = this.displayPadding + ((i % blocksPerRow) * (this.displayBlockWidth + (this.displayPadding * 2)));
const numRows = Math.ceil(this.scenes.length / blocksPerRow);
const row = numRows - Math.floor(i / blocksPerRow) - 1;
const y = this.displayPadding + this.displayHeight - ((row + 1) * (this.displayBlockWidth + (this.displayPadding * 2)));
if (this.scenes[i]) {
this.scenes[i].resize({ x, y, width: this.displayBlockWidth, height: this.displayBlockWidth, animate: false });
this.start();
} else {
this.scenes[i] = new BlockScene({ x, y, width: this.displayBlockWidth, height: this.displayBlockWidth, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, theme: this.themeService,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
colorFunction: this.getColorFunction() });
this.start();
}
}
}
}
compileShader(src, type): WebGLShader {
if (!this.gl) {
return;
}
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, src);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
console.log(this.gl.getShaderInfoLog(shader));
}
return shader;
}
buildShaderProgram(shaderInfo): WebGLProgram {
if (!this.gl) {
return;
}
const program = this.gl.createProgram();
shaderInfo.forEach((desc) => {
const shader = this.compileShader(desc.src, desc.type);
if (shader) {
this.gl.attachShader(program, shader);
}
});
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.log('Error linking shader program:');
console.log(this.gl.getProgramInfoLog(program));
}
return program;
}
start(): void {
this.running = true;
this.ngZone.runOutsideAngular(() => this.doRun());
}
doRun(): void {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
}
this.animationFrameRequest = requestAnimationFrame(() => this.run());
}
run(now?: DOMHighResTimeStamp): void {
if (!now) {
now = performance.now();
}
this.applyQueuedUpdates();
// skip re-render if there's no change to the scene
if (this.scenes.length && this.gl) {
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
// frame timestamp
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
if (this.vertexArray.dirty) {
/* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => {
this.gl.vertexAttribPointer(attribs[key].pointer,
attribs[key].count, // number of primitives in this attribute
this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
false, // never normalised
stride, // distance between values of the same attribute
attribs[key].offset); // offset of the first value
});
const pointArray = this.vertexArray.getVertexData();
if (pointArray.length) {
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
}
this.vertexArray.dirty = false;
} else {
const pointArray = this.vertexArray.getVertexData();
if (pointArray.length) {
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
}
}
if (this.readyNextFrame) {
this.readyNextFrame = false;
this.readyEvent.emit();
}
}
/* LOOP */
if (this.running && this.scenes.length && now <= (this.scenes.reduce((max, scene) => scene.animateUntil > max ? scene.animateUntil : max, 0) + 500)) {
this.doRun();
} else {
if (this.animationHeartBeat) {
clearTimeout(this.animationHeartBeat);
}
this.animationHeartBeat = window.setTimeout(() => {
this.start();
}, 1000);
}
}
@HostListener('document:click', ['$event'])
clickAway(event) {
if (!this.elRef.nativeElement.contains(event.target)) {
const currentPreview = this.selectedTx || this.hoverTx;
if (currentPreview) {
for (const scene of this.scenes) {
if (scene) {
scene.setHover(currentPreview, false);
}
}
this.start();
}
this.hoverTx = null;
this.selectedTx = null;
this.onTxHover(null);
}
}
@HostListener('pointerup', ['$event'])
onClick(event) {
if (!this.canvas) {
return;
}
if (event.target === this.canvas.nativeElement && event.pointerType === 'touch') {
this.setPreviewTx(event.offsetX, event.offsetY, true);
} else if (event.target === this.canvas.nativeElement) {
const keyMod = event.shiftKey || event.ctrlKey || event.metaKey;
const middleClick = event.which === 2 || event.button === 1;
this.onTxClick(event.offsetX, event.offsetY, keyMod || middleClick);
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
if (!this.canvas) {
return;
}
if (event.target === this.canvas.nativeElement) {
this.setPreviewTx(event.offsetX, event.offsetY, false);
} else {
this.onPointerLeave(event);
}
}
@HostListener('pointerleave', ['$event'])
onPointerLeave(event) {
if (event.pointerType !== 'touch') {
this.setPreviewTx(-1, -1, true);
}
}
setPreviewTx(cssX: number, cssY: number, clicked: boolean = false) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
if (!this.selectedTx || clicked) {
this.tooltipPosition = {
x: cssX,
y: cssY
};
const currentPreview = this.selectedTx || this.hoverTx;
let selected;
for (const scene of this.scenes) {
if (scene) {
selected = scene.getTxAt({ x, y: this.displayHeight - y });
if (selected) {
break;
}
}
}
if (selected !== currentPreview) {
if (currentPreview) {
for (const scene of this.scenes) {
if (scene) {
scene.setHover(currentPreview, false);
break;
}
}
this.start();
}
if (selected) {
for (const scene of this.scenes) {
if (scene) {
scene.setHover(selected, true);
break;
}
}
this.start();
if (clicked) {
this.selectedTx = selected;
} else {
this.hoverTx = selected;
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
}
} else {
if (clicked) {
this.selectedTx = null;
}
this.hoverTx = null;
this.onTxHover(null);
}
} else if (clicked) {
if (selected === this.selectedTx) {
this.hoverTx = this.selectedTx;
this.selectedTx = null;
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
} else {
this.selectedTx = selected;
}
}
}
}
updateSearchHighlight(): void {
if (this.highlightTx && this.highlightTx.txid !== this.searchText) {
for (const scene of this.scenes) {
if (scene) {
scene.setHighlight(this.highlightTx, false);
}
}
this.start();
} else if (this.searchText && this.searchText.length === 64) {
for (const scene of this.scenes) {
if (scene) {
const highlightTx = scene.txs[this.searchText];
if (highlightTx) {
scene.setHighlight(highlightTx, true);
this.highlightTx = highlightTx;
this.start();
}
}
}
}
}
setHighlightingEnabled(enabled: boolean): void {
for (const scene of this.scenes) {
scene.setHighlighting(enabled);
}
this.start();
}
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
for (const scene of this.scenes) {
if (scene) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
const selected = scene.getTxAt({ x, y: this.displayHeight - y });
if (selected && selected.txid) {
this.txClickEvent.emit({ tx: selected, keyModifier });
return;
}
}
}
}
onTxHover(hoverId: string) {
this.txHoverEvent.emit(hoverId);
}
getColorFunction(): ((tx: TxView) => Color) {
if (this.overrideColors) {
return this.overrideColors;
} else if (this.filterFlags) {
return this.getFilterColorFunction(this.filterFlags, this.gradientMode);
} else if (this.activeFilterFlags) {
return this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode);
} else {
return this.getFilterColorFunction(0n, this.gradientMode);
}
}
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
return (tx: TxView) => {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
} else {
return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
}
} else {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
tx,
defaultColors.unmatchedfee,
unmatchedAuditColors,
this.relativeTime || (Date.now() / 1000)
);
} else {
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : contrastColorFunction(
tx,
contrastColors.unmatchedfee,
unmatchedContrastAuditColors,
this.relativeTime || (Date.now() / 1000)
);
}
}
};
}
}
// WebGL shader attributes
const attribs = {
bounds: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
};
// Calculate the number of bytes per vertex based on specified attributes
const stride = Object.values(attribs).reduce((total, attrib) => {
return total + (attrib.count * 4);
}, 0);
// Calculate vertex attribute offsets
for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
const attrib = Object.values(attribs)[i];
attrib.offset = offset;
offset += (attrib.count * 4);
}
const vertShaderSrc = `
varying lowp vec4 vColor;
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
// shader interpolates between start and end values at the given rate, from the given time
attribute vec4 bounds;
attribute vec4 posX;
attribute vec4 posY;
attribute vec4 colR;
attribute vec4 colG;
attribute vec4 colB;
attribute vec4 colA;
uniform vec2 screenSize;
uniform float now;
float smootherstep(float x) {
x = clamp(x, 0.0, 1.0);
float ix = 1.0 - x;
x = x * x;
return x / (x + ix * ix);
}
float interpolateAttribute(vec4 attr) {
float d = (now - attr.z) * attr.w;
float delta = smootherstep(d);
return mix(attr.x, attr.y, delta);
}
void main() {
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
vec2 position = clamp(vec2(interpolateAttribute(posX), interpolateAttribute(posY)), bounds.xy, bounds.zw);
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
float red = interpolateAttribute(colR);
float green = interpolateAttribute(colG);
float blue = interpolateAttribute(colB);
float alpha = interpolateAttribute(colA);
vColor = vec4(red, green, blue, alpha);
}
`;
const fragShaderSrc = `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
// premultiply alpha
gl_FragColor.rgb *= gl_FragColor.a;
}
`;

View File

@@ -40,7 +40,7 @@
</tr>
<tr>
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="value">{{ fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> &nbsp; <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="fee"></app-fiat></span>
<td class="value">{{ fee | number }} <span class="symbol" i18n="shared.sats">sats</span> &nbsp; <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="fee"></app-fiat></span>
</tr>
<tr>
<td class="label" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
@@ -79,6 +79,11 @@
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
</ng-container>
<span *ngSwitchCase="'deprioritized'" class="badge badge-warning" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
<ng-container *ngSwitchCase="'added_deprioritized'">
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
</ng-container>
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>

View File

@@ -53,6 +53,13 @@
<td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
@if (block.extras.pool.minerNames[1].length > 16) {
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
} @else {
{{ block.extras.pool.minerNames[1] }}
}
</span>
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
{{ block.extras.pool.name }}
</a>
@@ -60,8 +67,15 @@
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
[class]="!block?.extras.pool.name || block?.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block?.extras.pool.name }}
</span>
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
@if (block.extras.pool.minerNames[1].length > 16) {
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
} @else {
{{ block.extras.pool.minerNames[1] }}
}
</span>
{{ block.extras.pool.name }}
</span>
</td>
</tr>
</tbody>

View File

@@ -137,7 +137,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
})
),
this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
return of([]);
}))

View File

@@ -66,10 +66,10 @@
[class.badge-success]="blockAudit?.matchRate >= 99"
[class.badge-warning]="blockAudit?.matchRate >= 75 && blockAudit?.matchRate < 99"
[class.badge-danger]="blockAudit?.matchRate < 75"
*ngIf="blockAudit?.matchRate != null; else nullHealth"
*ngIf="blockAudit?.matchRate != null && blockAudit?.id === block.id; else nullHealth"
>{{ blockAudit?.matchRate }}%</span>
<ng-template #nullHealth>
<ng-container *ngIf="!isLoadingOverview; else loadingHealth">
<ng-container *ngIf="!isLoadingOverview && blockAudit?.id === block.id; else loadingHealth">
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
</ng-container>
</ng-template>
@@ -182,6 +182,13 @@
<td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
@if (block.extras.pool.minerNames[1].length > 16) {
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
} @else {
{{ block.extras.pool.minerNames[1] }}
}
</span>
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
{{ block.extras.pool.name }}
</a>

View File

@@ -81,6 +81,19 @@ h1 {
}
}
.miner-name {
margin-right: 4px;
vertical-align: top;
}
.pool-logo {
width: 25px;
height: 25px;
position: relative;
top: -1px;
margin-right: 2px;
}
.row {
flex-direction: column;
@media (min-width: 768px) {

View File

@@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service';
import { ServicesApiServices } from '../../services/services-api.service';
import { PreloadService } from '../../services/preload.service';
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
@Component({
selector: 'app-block',
@@ -318,7 +319,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.accelerationsSubscription = this.block$.pipe(
switchMap((block) => {
return this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
return of([]);
}))
@@ -326,7 +327,7 @@ export class BlockComponent implements OnInit, OnDestroy {
})
).subscribe((accelerations) => {
this.accelerations = accelerations;
if (accelerations.length) {
if (accelerations.length && this.strippedTransactions) { // Don't call setupBlockAudit if we don't have transactions yet; it will be called later in overviewSubscription
this.setupBlockAudit();
}
});
@@ -524,6 +525,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isUnseen = {};
const isAdded = {};
const isPrioritized = {};
const isDeprioritized = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
@@ -535,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy {
this.numUnexpected = 0;
if (blockAudit?.template) {
// augment with locally calculated *de*prioritized transactions if possible
const { prioritized, deprioritized } = identifyPrioritizedTransactions(transactions);
// but if the local calculation produces returns unexpected results, don't use it
let useLocalDeprioritized = deprioritized.length < (transactions.length * 0.1);
for (const tx of prioritized) {
if (!isPrioritized[tx] && !isAccelerated[tx]) {
useLocalDeprioritized = false;
break;
}
}
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
if (tx.acc) {
@@ -550,9 +563,14 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.prioritizedTxs || []) {
for (const txid of blockAudit.prioritizedTxs) {
isPrioritized[txid] = true;
}
if (useLocalDeprioritized) {
for (const txid of deprioritized || []) {
isDeprioritized[txid] = true;
}
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
@@ -608,6 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy {
} else {
tx.status = 'prioritized';
}
} else if (isDeprioritized[tx.txid]) {
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
tx.status = 'added_deprioritized';
} else {
tx.status = 'deprioritized';
}
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {

View File

@@ -60,9 +60,14 @@
</ng-container>
</div>
<div class="animated" *ngIf="block.extras?.pool != undefined && showPools">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
{{ block.extras.pool.name}}
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge" [class.miner-name]="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
<ng-container *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''; else centralisedPool">
<img [ngbTooltip]="block.extras.pool.name" class="pool-logo faded" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
{{ block.extras.pool.minerNames[1] }}
</ng-container>
<ng-template #centralisedPool>
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'"> {{ block.extras.pool.name }}
</ng-template>
</a>
</div>
</div>

View File

@@ -19,6 +19,38 @@
pointer-events: none;
}
.on-pool-name-text {
display: inline-block;
padding-top: 2px;
font-weight: normal;
}
.on-pool {
align-items: center;
background-color: var(--bg);
display: inline-block;
margin-top: 4px;
padding: .25em .4em;
border-radius: .25rem;
}
.on-pool-container {
align-items: center;
position: relative;
top: -8px;
display: flex;
flex-direction: column;
}
.on-pool-container.selected {
top: 0px;
}
.pool-container {
margin-top: 12px;
}
.mined-block {
position: absolute;
top: 0px;
@@ -155,9 +187,16 @@
.badge {
position: relative;
top: 15px;
top: 19px;
z-index: 101;
color: #FFF;
overflow: hidden;
text-overflow: ellipsis;
max-width: 145px;
&.miner-name {
max-width: 125px;
}
}
.pool-logo {
@@ -168,6 +207,10 @@
margin-right: 2px;
}
.pool-logo.faded {
filter: grayscale(100%) brightness(1.5);
}
.animated {
transition: all 0.15s ease-in-out;
white-space: nowrap;

View File

@@ -1,8 +1,11 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !isMempoolModule}">
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div *ngIf="!widget" class="float-left" style="display: flex; width: 100%; align-items: center;">
<h1 i18n="master-page.blocks">Blocks</h1>
<app-svg-images name="blocks-2-3" style="width: 275px; max-width: 90%; margin-top: -10px"></app-svg-images>
<div *ngIf="!widget && isLoading" class="spinner-border" role="status"></div>
</div>
<div class="clearfix"></div>

View File

@@ -1,7 +1,9 @@
.spinner-border {
height: 25px;
width: 25px;
margin-top: 13px;
margin-top: -10px;
margin-left: -13px;
flex-shrink: 0;
}
.container-xl {

View File

@@ -12,7 +12,7 @@
<div class="input-group-prepend">
<span class="input-group-text">{{ currency$ | async }}</span>
</div>
<input type="text" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
<input type="text" inputmode="numeric" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div>
@@ -20,7 +20,7 @@
<div class="input-group-prepend">
<span class="input-group-text">BTC</span>
</div>
<input type="text" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
<input type="text" inputmode="numeric" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div>
@@ -28,7 +28,7 @@
<div class="input-group-prepend">
<span class="input-group-text" i18n="shared.sats">sats</span>
</div>
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
<input type="text" inputmode="numeric" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
</div>
</form>

View File

@@ -77,7 +77,7 @@ export class DifficultyMiningComponent implements OnInit {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
remainingBlocks: da.remainingBlocks - 1,
remainingBlocks: da.remainingBlocks,
colorAdjustments,
colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight,

View File

@@ -153,8 +153,8 @@ export class DifficultyComponent implements OnInit {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
minedBlocks: this.currentIndex + 1,
remainingBlocks: da.remainingBlocks - 1,
minedBlocks: this.currentIndex,
remainingBlocks: da.remainingBlocks,
expectedBlocks: Math.floor(da.expectedBlocks),
colorAdjustments,
colorPreviousAdjustments,

View File

@@ -1,23 +1,24 @@
<app-block-overview-multi
#blockGraph
[isLoading]="isLoadingTransactions"
[numBlocks]="numBlocks"
[padding]="padding"
[blockWidth]="blockWidth"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
></app-block-overview-multi>
<div class="blocks" [class.wrap]="wrapBlocks">
<ng-container *ngFor="let i of blockIndices">
<div class="block-wrapper" [style]="wrapperStyle">
<div class="block-container" [style]="containerStyle">
<app-block-overview-graph
#blockGraph
[isLoading]="false"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
[disableSpinner]="true"
[relativeTime]="blockInfo[i]?.timestamp"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
<h1 class="height">{{ blockInfo[i].height }}</h1>
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }} <img class="pool-logo" [src]="'/resources/mining-pools/' + blockInfo[i].extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'"> </h2>
</div>
</div>
</div>

View File

@@ -1,4 +1,7 @@
.blocks {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
min-width: 100vw;
@@ -66,4 +69,12 @@
.block-container {
overflow: hidden;
}
}
.pool-logo {
width: 1.2em;
height: 1.2em;
position: relative;
top: -1px;
margin-right: 2px;
}

View File

@@ -1,16 +1,16 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError, startWith } from 'rxjs/operators';
import { catchError } from 'rxjs/operators';
import { Subject, Subscription, of } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
import { CacheService } from '../../services/cache.service';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
@@ -48,24 +48,27 @@ interface BlockInfo extends BlockExtended {
})
export class EightBlocksComponent implements OnInit, OnDestroy {
network = '';
latestBlocks: BlockExtended[] = [];
latestBlocks: (BlockExtended | null)[] = [];
pendingBlocks: Record<number, ((b: BlockExtended) => void)[]> = {};
isLoadingTransactions = true;
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
webGlEnabled = true;
hoverTx: string | null = null;
blocksSubscription: Subscription;
tipSubscription: Subscription;
cacheBlocksSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
height: number = 0;
numBlocks: number = 8;
autoNumBlocks: boolean = false;
blockIndices: number[] = [...Array(8).keys()];
autofit: boolean = false;
padding: number = 0;
wrapBlocks: boolean = false;
blockWidth: number = 1080;
blockWidth: number = 360;
animationDuration: number = 2000;
animationOffset: number = 0;
stagger: number = 0;
@@ -79,13 +82,14 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
wrapperStyle = {
'--block-width': '1080px',
width: '1080px',
height: '1080px',
maxWidth: '1080px',
padding: '',
margin: '',
};
containerStyle = {};
resolution: number = 86;
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
@ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
constructor(
private route: ActivatedRoute,
@@ -93,6 +97,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
public stateService: StateService,
private websocketService: WebsocketService,
private apiService: ApiService,
private cacheService: CacheService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
@@ -103,15 +108,24 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
this.network = this.stateService.network;
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
this.blockIndices = [...Array(this.numBlocks).keys()];
this.autofit = params.autofit !== 'false';
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 10;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 0;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 320;
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 4;
this.wrapBlocks = params.wrap !== 'false';
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
this.animationOffset = this.padding * 2;
this.animationOffset = 0;
if (!this.numBlocks) {
this.autoNumBlocks = true;
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
this.numBlocks = Math.floor(width / paddedWidth) * Math.floor(height / paddedWidth);
}
this.blockIndices = [...Array(this.numBlocks).keys()];
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
@@ -122,24 +136,24 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
height: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
padding: (this.padding || 0) +'px 0px',
margin: (this.padding || 0) +'px ',
};
if (params.test === 'true') {
if (this.blocksSubscription) {
this.blocksSubscription.unsubscribe();
this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block: BlockExtended) => {
if (this.pendingBlocks[block.height]) {
this.pendingBlocks[block.height].forEach(resolve => resolve(block));
delete this.pendingBlocks[block.height];
}
this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
this.tipSubscription?.unsubscribe();
this.tipSubscription = this.stateService.chainTip$
.subscribe((height) => {
this.height = height;
this.handleNewBlock(height);
});
this.shiftTestBlocks();
} else if (!this.blocksSubscription) {
this.blocksSubscription = this.stateService.blocks$
.subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
}
});
this.setupBlockGraphs();
@@ -149,53 +163,80 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
}
ngAfterViewInit(): void {
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
this.setupBlockGraphs();
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
if (this.autoNumBlocks) {
this.autoNumBlocks = true;
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
this.numBlocks = Math.floor(width / paddedWidth) * Math.floor(height / paddedWidth);
this.blockIndices = [...Array(this.numBlocks).keys()];
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
height: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
margin: (this.padding || 0) +'px ',
};
if (this.cacheBlocksSubscription) {
this.cacheBlocksSubscription.unsubscribe();
}
this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block: BlockExtended) => {
if (this.pendingBlocks[block.height]) {
this.pendingBlocks[block.height].forEach(resolve => resolve(block));
delete this.pendingBlocks[block.height];
}
});
this.tipSubscription?.unsubscribe();
this.tipSubscription = this.stateService.chainTip$
.subscribe((height) => {
this.height = height;
this.handleNewBlock(height);
});
this.setupBlockGraphs();
});
}
}
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
if (this.blocksSubscription) {
this.blocksSubscription?.unsubscribe();
if (this.tipSubscription) {
this.tipSubscription?.unsubscribe();
}
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
}
shiftTestBlocks(): void {
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
sub.unsubscribe();
this.handleNewBlock(result.slice(0, this.numBlocks));
this.testHeight++;
clearTimeout(this.testShiftTimeout);
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
});
}
async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
async handleNewBlock(height: number): Promise<void> {
const readyPromises: Promise<TransactionStripped[]>[] = [];
const previousBlocks = this.latestBlocks;
const blocks = await this.loadBlocks(height, this.numBlocks);
const newHeights = {};
this.latestBlocks = blocks;
for (const block of blocks) {
newHeights[block.height] = true;
if (!this.strippedTransactions[block.height]) {
readyPromises.push(new Promise((resolve) => {
const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
catchError(() => {
return of([]);
}),
).subscribe((transactions) => {
this.strippedTransactions[block.height] = transactions;
subscription.unsubscribe();
resolve(transactions);
});
}));
readyPromises.push(this.loadBlockTransactions(block));
}
}
await Promise.allSettled(readyPromises);
this.isLoadingTransactions = false;
this.updateBlockGraphs(blocks);
// free up old transactions
@@ -206,12 +247,44 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
});
}
async loadBlocks(height: number, numBlocks: number): Promise<BlockExtended[]> {
const promises: Promise<BlockExtended>[] = [];
for (let i = 0; i < numBlocks; i++) {
this.cacheService.loadBlock(height - i);
const cachedBlock = this.cacheService.getCachedBlock(height - i);
if (cachedBlock) {
promises.push(Promise.resolve(cachedBlock));
} else {
promises.push(new Promise((resolve) => {
if (!this.pendingBlocks[height - i]) {
this.pendingBlocks[height - i] = [];
}
this.pendingBlocks[height - i].push(resolve);
}));
}
}
return Promise.all(promises);
}
async loadBlockTransactions(block: BlockExtended): Promise<TransactionStripped[]> {
return new Promise((resolve) => {
this.apiService.getStrippedBlockTransactions$(block.id).pipe(
catchError(() => {
return of([]);
}),
).subscribe((transactions) => {
this.strippedTransactions[block.height] = transactions;
resolve(transactions);
});
});
}
updateBlockGraphs(blocks): void {
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
});
if (this.blockGraph) {
for (let i = 0; i < this.numBlocks; i++) {
this.blockGraph.replace(i, this.strippedTransactions[blocks?.[this.getBlockIndex(i)]?.height] || [], 'right', false, startTime + (this.stagger * i));
}
}
this.showInfo = false;
setTimeout(() => {
@@ -226,28 +299,22 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
}
setupBlockGraphs(): void {
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.destroy();
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
});
if (this.blockGraph) {
for (let i = 0; i < this.numBlocks; i++) {
this.blockGraph.destroy(i);
this.blockGraph.setup(i, this.strippedTransactions[this.latestBlocks?.[this.getBlockIndex(i)]?.height] || []);
}
}
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
getBlockIndex(slotIndex: number): number {
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
const blocksPerRow = Math.floor(width / paddedWidth);
const blocksPerColumn = Math.floor(height / paddedWidth);
const row = Math.floor(slotIndex / blocksPerRow);
const column = slotIndex % blocksPerRow;
return (blocksPerColumn - 1 - row) * blocksPerRow + column;
}
}

View File

@@ -0,0 +1,15 @@
<app-block-overview-multi
#blockGraph
[isLoading]="isLoading"
[numBlocks]="numBlocks"
[padding]="padding"
[blockWidth]="blockWidth"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'left'"
[flip]="true"
[showFilters]="true"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
(txClickEvent)="onTxClick($event)"
></app-block-overview-multi>

View File

@@ -0,0 +1,72 @@
.blocks {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
min-width: 100vw;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
&.wrap {
flex-wrap: wrap;
}
.block-wrapper {
flex-grow: 0;
flex-shrink: 0;
position: relative;
--block-width: 1080px;
.info {
position: absolute;
left: 8%;
top: 8%;
right: 8%;
bottom: 8%;
height: 84%;
width: 84%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: calc(var(--block-width) * 0.03);
text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
h1 {
font-size: 6em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
h2 {
font-size: 1.8em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.hash {
font-family: monospace;
word-wrap: break-word;
font-size: 1.4em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.mined-by {
position: absolute;
bottom: 0;
margin: auto;
text-align: center;
}
}
}
.block-container {
overflow: hidden;
}
}

View File

@@ -0,0 +1,249 @@
import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { detectWebGL } from '../../shared/graphs.utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
import { CacheService } from '../../services/cache.service';
import { isMempoolDelta, MempoolBlockDelta } from '../../interfaces/websocket.interface';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
@Component({
selector: 'app-eight-mempool',
templateUrl: './eight-mempool.component.html',
styleUrls: ['./eight-mempool.component.scss'],
animations: [
trigger('infoChange', [
transition(':enter', [
style({ opacity: 0 }),
animate('1000ms', style({ opacity: 1 })),
]),
transition(':leave', [
animate('1000ms 500ms', style({ opacity: 0 }))
])
]),
],
})
export class EightMempoolComponent implements OnInit, OnDestroy {
network = '';
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
isLoading = true;
webGlEnabled = true;
hoverTx: string | null = null;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
blockSub: Subscription;
chainDirection: string = 'right';
poolDirection: string = 'left';
lastBlockHeight: number = 0;
lastBlockHeightUpdate: number[] = [];
numBlocks: number = 8;
autoNumBlocks: boolean = false;
blockIndices: number[] = [];
autofit: boolean = false;
padding: number = 0;
wrapBlocks: boolean = false;
blockWidth: number = 360;
animationDuration: number = 2000;
animationOffset: number = 0;
stagger: number = 0;
testing: boolean = true;
testHeight: number = 800000;
testShiftTimeout: number;
showInfo: boolean = true;
wrapperStyle = {
'--block-width': '1080px',
width: '1080px',
height: '1080px',
maxWidth: '1080px',
margin: '',
};
containerStyle = {};
resolution: number = 86;
@ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
constructor(
private route: ActivatedRoute,
private router: Router,
public stateService: StateService,
private websocketService: WebsocketService,
private apiService: ApiService,
private cacheService: CacheService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
}
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks']);
this.network = this.stateService.network;
this.stateService.activeGoggles$.next({ mode: 'and', filters: [], gradient: 'fee' });
this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => {
// process update
if (isMempoolDelta(update)) {
// delta
this.updateBlock(update);
} else {
const transactionsStripped = update.transactions;
const inOldBlock = {};
const inNewBlock = {};
const added: TransactionStripped[] = [];
const changed: { txid: string, rate: number | undefined, flags: number, acc: boolean | undefined }[] = [];
const removed: string[] = [];
for (const tx of transactionsStripped) {
inNewBlock[tx.txid] = true;
}
for (const txid of Object.keys(this.blockGraph?.scenes[this.numBlocks - update.block - 1]?.txs || {})) {
inOldBlock[txid] = true;
if (!inNewBlock[txid]) {
removed.push(txid);
}
}
for (const tx of transactionsStripped) {
if (!inOldBlock[tx.txid]) {
added.push(tx);
} else {
changed.push({
txid: tx.txid,
rate: tx.rate,
flags: tx.flags,
acc: tx.acc
});
}
}
this.updateBlock({
block: update.block,
removed,
changed,
added
});
}
});
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.autofit = params.autofit !== 'false';
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 0;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 320;
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 4;
this.wrapBlocks = params.wrap !== 'false';
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
this.animationOffset = 0;
if (!this.numBlocks) {
this.autoNumBlocks = true;
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
this.numBlocks = Math.floor(width / paddedWidth) * Math.floor(height / paddedWidth);
}
this.blockIndices = [...Array(this.numBlocks).keys()];
this.lastBlockHeightUpdate = this.blockIndices.map(() => 0);
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
height: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
margin: (this.padding || 0) +'px ',
};
this.websocketService.startTrackMempoolBlocks(this.blockIndices);
});
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
if (this.autoNumBlocks) {
this.autoNumBlocks = true;
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
this.numBlocks = Math.floor(width / paddedWidth) * Math.floor(height / paddedWidth);
this.blockIndices = [...Array(this.numBlocks).keys()];
this.lastBlockHeightUpdate = this.blockIndices.map(() => 0);
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
height: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
margin: (this.padding || 0) +'px ',
};
this.websocketService.startTrackMempoolBlocks(this.blockIndices);
}
}
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
this.blockSub.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
}
updateBlock(delta: MempoolBlockDelta): void {
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeightUpdate[delta.block]);
if (blockMined) {
this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
} else {
this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], this.poolDirection);
}
this.isLoading = false;
this.lastBlockHeightUpdate[delta.block] = this.stateService.latestBlockHeight;
}
}

View File

@@ -5,7 +5,7 @@
</div>
<div class="faucet-container text-center">
@if (txid) {
<div class="alert alert-success w-100 text-truncate">
<fa-icon [icon]="['fas', 'circle-check']"></fa-icon>
@@ -27,6 +27,14 @@
<app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login>
</div>
}
@else if (user && user.status === 'pending' && !user.email && user.snsId) {
<div class="alert alert-danger w-100 col d-flex justify-content-center text-left">
<span class="d-flex">
<fa-icon [icon]="['fas', 'exclamation-triangle']" [fixedWidth]="true" class="mr-1"></fa-icon>
<span>Please <a class="text-primary" [routerLink]="['/services/account/settings']">verify your account</a> by providing a valid email address. To mitigate spam, we delete unverified accounts at regular intervals.</span>
</span>
</div>
}
@else if (error === 'not_available') {
<!-- User logged in but not a paid user or did not link its Twitter account -->
<div class="alert alert-mempool d-block text-center w-100">
@@ -36,6 +44,13 @@
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
</div>
}
@else if (error === 'account_limited') {
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span>
</div>
</div>
}
@else if (error) {
<!-- User can request -->
<app-mempool-error class="w-100" [error]="error"></app-mempool-error>
@@ -81,7 +96,7 @@
}
<!-- Send back coins -->
@if (status?.address) {
@if (status?.address) {
<div class="mt-4 alert alert-info w-100">If you no longer need your testnet4 coins, please consider <a class="text-primary" [routerLink]="['/address/' | relativeUrl, status.address]"><u>sending them back</u></a> to replenish the faucet.</div>
}

View File

@@ -1,7 +1,6 @@
import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core";
import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";
import { Subscription } from "rxjs";
import { StorageService } from "../../services/storage.service";
import { ServicesApiServices } from "../../services/services-api.service";
import { getRegex } from "../../shared/regex.utils";
import { StateService } from "../../services/state.service";
@@ -19,7 +18,7 @@ export class FaucetComponent implements OnInit, OnDestroy {
error: string = '';
user: any = undefined;
txid: string = '';
faucetStatusSubscription: Subscription;
status: {
min: number; // minimum amount to request at once (in sats)
@@ -34,7 +33,6 @@ export class FaucetComponent implements OnInit, OnDestroy {
constructor(
private cd: ChangeDetectorRef,
private storageService: StorageService,
private servicesApiService: ServicesApiServices,
private formBuilder: FormBuilder,
private stateService: StateService,
@@ -56,14 +54,17 @@ export class FaucetComponent implements OnInit, OnDestroy {
}
ngOnInit() {
this.user = this.storageService.getAuth()?.user ?? null;
if (!this.user) {
this.loading = false;
return;
}
// Setup form
this.updateFaucetStatus();
this.servicesApiService.userSubject$.subscribe(user => {
this.user = user;
if (!user) {
this.loading = false;
this.cd.markForCheck();
return;
}
// Setup form
this.updateFaucetStatus();
this.cd.markForCheck();
});
// Track transaction
this.websocketService.want(['blocks', 'mempool-blocks']);
@@ -145,9 +146,6 @@ export class FaucetComponent implements OnInit, OnDestroy {
'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4')), this.getNotFaucetAddressValidator(faucetAddress)]],
'satoshis': [min, [Validators.required, Validators.min(min), Validators.max(max)]]
});
this.loading = false;
this.cd.markForCheck();
}
updateForm(min, max, faucetAddress: string): void {
@@ -160,6 +158,8 @@ export class FaucetComponent implements OnInit, OnDestroy {
this.faucetForm.get('satoshis').updateValueAndValidity();
this.faucetForm.get('satoshis').markAsDirty();
}
this.loading = false;
this.cd.markForCheck();
}
setAmount(value: number): void {

View File

@@ -5,7 +5,7 @@
<div *ngIf="widget">
<div class="pool-distribution" *ngIf="(hashrateObservable$ | async) as hashrates; else loadingStats">
<div class="item">
<h5 class="card-title" i18n="mining.hashrate">Hashrate</h5>
<h5 class="card-title" i18n="mining.hashrate">Hashrate (1w)</h5>
<p class="card-text">
{{ hashrates.currentHashrate | amountShortener: 1 : 'H/s' }}
</p>

View File

@@ -85,7 +85,6 @@
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home" *ngIf="network.val === '' && stateService.env.ACCELERATOR">
<a class="nav-link" [routerLink]="['/acceleration' | relativeUrl]" (click)="collapse()">
<fa-icon [icon]="['fas', 'rocket']" [fixedWidth]="true" i18n-title="master-page.accelerator-dashboard" title="Accelerator Dashboard"></fa-icon>
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
</a>
</li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">

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