Compare commits

...

309 Commits

Author SHA1 Message Date
natsoni
c6285dfa26 Merge branch 'master' into natsoni/fix-tooltip-timeline 2024-10-25 11:52:25 +02:00
wiz
8b01a83948 Increase time of demo mode from 3s to 15s 2024-10-25 16:49:24 +09:00
wiz
2ceb9001a1 Merge pull request #5613 from mempool/mononaut/fix-purple-pie-chart
fix purple pie chart with single pool
2024-10-25 15:13:06 +09:00
wiz
d44b7926d2 Merge pull request #5611 from mempool/nymkappa/demo-mode-but-it-does-not-crash-the-tv-anymore-well-maybe-we-ll-see-about-this
[demo] better? demo mode
2024-10-25 14:48:59 +09:00
wiz
57299e086e Remove /graphs from demo routes 2024-10-25 14:45:45 +09:00
Mononaut
c1d17dac43 fix purple pie chart with single pool 2024-10-25 05:39:52 +00:00
wiz
cb49f9d929 Merge pull request #5612 from mempool/nymkappa/internal-core-rpc-proxy
[core routes] /api/internal -> /api/v1/internal
2024-10-25 14:09:03 +09:00
nymkappa
185be3d598 [core routes] use config.MEMPOOL.API_URL_PREFIX 2024-10-25 14:02:09 +09:00
nymkappa
c950e3d0ae [core routes] /api/internal -> /api/v1/internal 2024-10-25 13:54:40 +09:00
nymkappa
99cc47cf00 [demo] better? demo mode 2024-10-24 16:52:47 +09:00
wiz
908b8b4352 Merge pull request #5606 from mempool/nymkappa/configurable-prod-domains
make prod domains configurable
2024-10-23 22:58:56 +09:00
nymkappa
1a7f475220 make prod domains configurable 2024-10-23 22:51:04 +09:00
wiz
cb63d17a2f ops: Don't always set frameoptions in nginx 2024-10-23 22:12:40 +09:00
wiz
c8ce4631e2 Merge pull request #5605 from mempool/mononaut/tx-extras-module
Refactor transaction page component
2024-10-23 22:00:39 +09:00
Mononaut
96c2b0a2f7 fix cpfp button 2024-10-23 12:28:40 +00:00
Mononaut
23475c7a1b Refactor transaction page component 2024-10-23 08:56:27 +00:00
wiz
9ab3d3195e Merge pull request #5604 from mempool/wiz/use-typescript-buildtime-path-aliases
Use typescript path aliases for build time import path resolution
2024-10-23 12:01:36 +09:00
wiz
a22d07ae60 Rename some more relative paths to use @app path alias 2024-10-23 11:46:33 +09:00
wiz
221658f6bf Change @app/interfaces to new @interfaces path alias 2024-10-23 11:09:38 +09:00
wiz
133df2e4be Use typescript path aliases for build time import path resolution 2024-10-22 23:30:19 +09:00
wiz
5fba9595af Merge pull request #5601 from mempool/nymkappa/demo-mode
implement very simple demo mode
2024-10-22 15:16:45 +09:00
wiz
90ca77a46a Tweak demo mode to use all dashboards 2024-10-22 15:08:57 +09:00
nymkappa
f0c76c1349 implement very simple demo mode 2024-10-19 16:07:09 +09:00
wiz
6e5cfa9bf2 Merge pull request #4831 from mempool/mononaut/wallet
Add multi-address wallet page
2024-10-18 13:05:17 +09:00
wiz
9ffcf2eca5 ops: Enable wallets in prod mempool backend config 2024-10-18 12:32:42 +09:00
wiz
8514fb9bdc Merge branch 'master' into mononaut/wallet 2024-10-18 12:08:05 +09:00
wiz
c1be7460c0 Merge pull request #5375 from mempool/mononaut/wallet-dashboard-widgets
custom wallet dashboard widgets
2024-10-18 12:06:02 +09:00
Mononaut
602aa4f948 fix wallet merge conflicts 2024-10-18 03:02:30 +00:00
Mononaut
2d2c55ce0e Add link to wallet page from custom dashboard widget 2024-10-18 02:41:44 +00:00
Mononaut
f0e207dff2 fix wallet balance graph bug 2024-10-18 02:41:44 +00:00
Mononaut
756e4356a5 named wallet sync track txo stats 2024-10-18 02:41:44 +00:00
Mononaut
9c303e8c23 address wallet page by name 2024-10-18 02:41:43 +00:00
Mononaut
a7ba4a0be8 Add multi-address wallet page 2024-10-18 02:41:18 +00:00
Mononaut
54c2d7efe5 move custom wallet sync to services backend 2024-10-18 02:35:19 +00:00
Mononaut
3f8eb3a2cd check in new resources 2024-10-18 02:35:19 +00:00
Mononaut
e095192968 custom dashboard wallet widgets 2024-10-18 02:35:17 +00:00
Mononaut
862c9591a1 wallet tracking backend support 2024-10-18 02:34:25 +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
softsimon
510b4adcea Merge branch 'master' into natsoni/fix-tooltip-timeline 2024-10-04 21:59:07 -06: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
natsoni
0928d64e1e Fix timeline tooltip on mobile 2024-09-27 18:59:23 +02: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
383 changed files with 8313 additions and 3771 deletions

12
LICENSE
View File

@@ -1,5 +1,5 @@
The Mempool Open Source Project® 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 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 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®, The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo, 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 the mempool block visualization Logo, the mempool Blocks Logo, the mempool
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
of Mempool Space K.K in Japan, the United States, and/or other countries. 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 See our full Trademark Policy and Guidelines for more details, published on
<https://mempool.space/trademark-policy>. <https://mempool.space/trademark-policy>.

View File

@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build #### 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._ _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, "AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "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_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"AUDIT": false, "AUDIT": false,
"RUST_GBT": false, "RUST_GBT": true,
"LIMIT_GBT": false, "LIMIT_GBT": false,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6, "DISK_CACHE_BLOCK_INTERVAL": 6,
@@ -45,7 +46,8 @@
"PASSWORD": "mempool", "PASSWORD": "mempool",
"TIMEOUT": 60000, "TIMEOUT": 60000,
"COOKIE": false, "COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie" "COOKIE_PATH": "/path/to/bitcoin/.cookie",
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import rbfCache from './rbf-cache'; 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 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 matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template const added: string[] = []; // present in mined block, not in template
const unseen: string[] = []; // present in the mined block, not in our mempool const unseen: string[] = []; // present in the mined block, not in our mempool
const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone 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 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 rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator const accelerated: string[] = []; // prioritized by the mempool accelerator
@@ -133,23 +135,7 @@ class Audit {
totalWeight += tx.weight; totalWeight += tx.weight;
} }
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
// identify "prioritized" transactions
let lastEffectiveRate = 0;
// Iterate over the mined template from bottom to top (excluding the coinbase)
// Transactions should appear in ascending order of mining priority.
for (let i = transactions.length - 1; i > 0; i--) {
const blockTx = transactions[i];
// If a tx has a lower in-band effective fee rate than the previous tx,
// it must have been prioritized out-of-band (in order to have a higher mining priority)
// so exclude from the analysis.
if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) {
prioritized.push(blockTx.txid);
// accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference
} else if (!isAccelerated[blockTx.txid]) {
lastEffectiveRate = blockTx.effectiveFeePerVsize || 0;
}
}
// transactions missing from near the end of our template are probably not being censored // transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight); 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'; import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi { export interface AbstractBitcoinApi {
@@ -23,12 +23,14 @@ export interface AbstractBitcoinApi {
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>; $sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>; $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>; $getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>; $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>; $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
$getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]>;
startHealthChecks(): void; startHealthChecks(): void;
getHealthStatus(): HealthCheckHost[]; getHealthStatus(): HealthCheckHost[];

View File

@@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult {
}, },
['reject-reason']?: string, ['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 * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; 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 { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks'; import blocks from '../blocks';
import mempool from '../mempool'; 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> { async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return { return {
@@ -251,6 +255,10 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.$getRawTransaction(txids[0]); return this.$getRawTransaction(txids[0]);
} }
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
throw new Error('Method getAddressTransactionSummary not supported by the Bitcoin RPC API.');
}
$getEstimatedHashrate(blockHeight: number): Promise<number> { $getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core // 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight); return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
@@ -323,6 +331,7 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr', 'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard', 'nonstandard': 'nonstandard',
'multisig': 'multisig', 'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return' 'nulldata': 'op_return'
}; };

View File

@@ -1,6 +1,7 @@
import { Application, NextFunction, Request, Response } from 'express'; import { Application, NextFunction, Request, Response } from 'express';
import logger from '../../logger'; import logger from '../../logger';
import bitcoinClient from './bitcoin-client'; import bitcoinClient from './bitcoin-client';
import config from '../../config';
/** /**
* Define a set of routes used by the accelerator server * Define a set of routes used by the accelerator server
@@ -11,15 +12,15 @@ class BitcoinBackendRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
.get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
.get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
.post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
.post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept) .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
.get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
.get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
.get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
.get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
; ;
} }

View File

@@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository'; import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache'; import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp'; import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
class BitcoinRoutes { class BitcoinRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@@ -47,6 +48,8 @@ class BitcoinRoutes {
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .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', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', 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') { if (config.MEMPOOL.BACKEND !== 'esplora') {
@@ -86,7 +89,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json'); res.set('Content-Type', 'application/json');
res.send(result); res.send(result);
} catch (e) { } 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(); const result = mempoolBlocks.getMempoolBlocks();
res.json(result); res.json(result);
} catch (e) { } 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) { private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
res.status(500).send('Not an array'); handleError(req, res, 500, 'Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
@@ -128,12 +131,12 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> { private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids; const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') { if (!txids_csv || typeof txids_csv !== 'string') {
res.status(500).send('Invalid txids format'); handleError(req, res, 500, 'Invalid txids format');
return; return;
} }
const txids = txids_csv.split(','); const txids = txids_csv.split(',');
if (txids.length > 50) { if (txids.length > 50) {
res.status(400).send('Too many txids requested'); handleError(req, res, 400, 'Too many txids requested');
return; return;
} }
@@ -141,13 +144,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends); res.json(batchedOutspends);
} catch (e) { } 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) { private async $getCpfpInfo(req: Request, res: Response) {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { 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; return;
} }
@@ -180,7 +183,7 @@ class BitcoinRoutes {
try { try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) { } catch (e) {
res.status(500).send('failed to get CPFP info'); handleError(req, res, 500, 'failed to get CPFP info');
return; 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) { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; 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) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; 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 // Not modified
// 422 Unprocessable Entity // 422 Unprocessable Entity
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 // 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) { } catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
res.status(404).send(e.message); handleError(req, res, 404, e.message);
} else { } 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) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; 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.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions); res.json(transactions);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block); res.json(block);
} catch (e) { } 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.setHeader('content-type', 'text/plain');
res.send(blockHeader); res.send(blockHeader);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
return res.status(404).send(`audit not available`); handleError(req, res, 404, `audit not available`);
return;
} }
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
return res.status(404).send(`transaction audit not available`); handleError(req, res, 404, `transaction audit not available`);
return;
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
@@ -388,42 +393,49 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res); return await this.getLegacyBlocks(req, res);
} }
} catch (e) { } 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) { private async getBlocksByBulk(req: Request, res: Response) {
try { try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented 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) { 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()) { 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); const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) { 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); const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) { 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) { 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) { 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to)); res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks); res.json(returnBlocks);
} catch (e) { } 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) { private async getBlockTransactions(req: Request, res: Response) {
try { try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@@ -483,7 +495,7 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); 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)); const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash); res.send(blockHash);
} catch (e) { } 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) { private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { 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; return;
} }
@@ -507,15 +519,16 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { 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> { private async getAddressTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { 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; return;
} }
@@ -528,23 +541,23 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { 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; 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> { private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { 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; return;
} }
} }
private async getScriptHash(req: Request, res: Response) { private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { 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; return;
} }
@@ -555,15 +568,16 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { 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> { private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { 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; return;
} }
@@ -578,16 +592,16 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { 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; 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> { private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { 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; return;
} }
} }
@@ -597,7 +611,7 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash); res.send(blockHash);
} catch (e) { } 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(); const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool); res.send(rawMempool);
} catch (e) { } 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 { try {
const result = blocks.getCurrentBlockHeight(); const result = blocks.getCurrentBlockHeight();
if (!result) { 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.setHeader('content-type', 'text/plain');
res.send(result.toString()); res.send(result.toString());
} catch (e) { } 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.setHeader('content-type', 'text/plain');
res.send(result); res.send(result);
} catch (e) { } 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.setHeader('content-type', 'application/octet-stream');
res.send(result); res.send(result);
} catch (e) { } 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); const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result); res.json(result);
} catch (e) { } 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); const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result); res.json(result);
} catch (e) { } 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 replaces
}); });
} catch (e) { } 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); const result = rbfCache.getRbfTrees(false);
res.json(result); res.json(result);
} catch (e) { } 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); const result = rbfCache.getRbfTrees(true);
res.json(result); res.json(result);
} catch (e) { } 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(); res.status(204).send();
} }
} catch (e) { } 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); const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result); res.json(result);
} catch (e) { } 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) { if (da) {
res.json(da); res.json(da);
} else { } else {
res.status(503).send(`Service Temporarily Unavailable`); handleError(req, res, 503, `Service Temporarily Unavailable`);
} }
} catch (e) { } 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); const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } 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')); : (e.message || 'Error'));
} }
} }
@@ -764,7 +779,7 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex); const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } 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')); : (e.message || 'Error'));
} }
} }
@@ -776,8 +791,20 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result); res.send(result);
} catch (e: any) { } catch (e: any) {
res.setHeader('content-type', 'text/plain'); handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
res.status(400).send(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')); : (e.message || 'Error'));
} }
} }

View File

@@ -179,4 +179,11 @@ export namespace IEsploraApi {
burn_count: number; burn_count: number;
} }
export interface AddressTxSummary {
txid: string;
value: number;
height: number;
time: number;
tx_position?: number;
}
} }

View File

@@ -5,7 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger'; import logger from '../../logger';
import { Common } from '../common'; import { Common } from '../common';
import { TestMempoolAcceptResult } from './bitcoin-api.interface'; import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
@@ -305,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi {
} }
$getAddress(address: string): Promise<IEsploraApi.Address> { $getAddress(address: string): Promise<IEsploraApi.Address> {
throw new Error('Method getAddress not implemented.'); return this.failoverRouter.$get<IEsploraApi.Address>('/address/' + address);
} }
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> { $getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
@@ -332,6 +332,10 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.'); 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> { $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
} }
@@ -357,6 +361,10 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid); return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);
} }
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
return this.failoverRouter.$get<IEsploraApi.AddressTxSummary[]>('/address/' + address + '/txs/summary');
}
public startHealthChecks(): void { public startHealthChecks(): void {
this.failoverRouter.startHealthChecks(); this.failoverRouter.startHealthChecks();
} }

View File

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

View File

@@ -10,7 +10,6 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script'; import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings // Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@@ -80,8 +79,8 @@ export class Common {
return arr; return arr;
} }
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } { static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
const matches: { [txid: string]: MempoolTransactionExtended[] } = {}; const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
// For small N, a naive nested loop is extremely fast, but it doesn't scale // For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) { 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)); addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
}); });
if (foundMatches?.length) { if (foundMatches?.length) {
matches[addedTx.txid] = [...new Set(foundMatches)]; matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
} }
}); });
} else { } else {
@@ -124,7 +123,7 @@ export class Common {
foundMatches.add(deletedTx); foundMatches.add(deletedTx);
} }
if (foundMatches.size) { 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(); const replaced: Set<MempoolTransactionExtended> = new Set();
for (let i = 0; i < tx.vin.length; i++) { for (let i = 0; i < tx.vin.length; i++) {
const vin = tx.vin[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) { if (match && match.txid !== tx.txid) {
replaced.add(match); replaced.add(match);
// remove this tx from the spendMap // remove this tx from the spendMap
// prevents the same tx being replaced more than once // prevents the same tx being replaced more than once
for (const replacedVin of match.vin) { for (const replacedVin of match.vin) {
const key = `${replacedVin.txid}:${replacedVin.vout}`; const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
spendMap.delete(key); spendMap.delete(replacedKey);
} }
} }
const key = `${vin.txid}:${vin.vout}`;
spendMap.delete(key); spendMap.delete(key);
} }
if (replaced.size) { if (replaced.size) {
@@ -200,10 +199,13 @@ export class Common {
* *
* returns true early if any standardness rule is violated, otherwise false * 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) * (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 // version
if (tx.version > TX_MAX_STANDARD_VERSION) { if (this.isNonStandardVersion(tx, height)) {
return true; return true;
} }
@@ -250,6 +252,8 @@ export class Common {
} }
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true; return true;
} else if (this.isNonStandardAnchor(tx, height)) {
return true;
} }
// TODO: bad-witness-nonstandard // TODO: bad-witness-nonstandard
} }
@@ -335,6 +339,49 @@ export class Common {
return false; 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 { static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight; let weight = tx.weight;
let hasWitness = false; let hasWitness = false;
@@ -415,7 +462,7 @@ export class Common {
return flags; return flags;
} }
static getTransactionFlags(tx: TransactionExtended): number { static getTransactionFlags(tx: TransactionExtended, height?: number): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n; let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF) // Update variable flags (CPFP, RBF)
@@ -548,7 +595,7 @@ export class Common {
if (hasFakePubkey) { if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey; flags |= TransactionFlags.fake_pubkey;
} }
// fast but bad heuristic to detect possible coinjoins // 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) // (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; 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; flags |= TransactionFlags.batch_payout;
} }
if (this.isNonStandard(tx)) { if (this.isNonStandard(tx, height)) {
flags |= TransactionFlags.nonstandard; flags |= TransactionFlags.nonstandard;
} }
return Number(flags); return Number(flags);
} }
static classifyTransaction(tx: TransactionExtended): TransactionClassified { static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
let flags = 0; let flags = 0;
try { try {
flags = Common.getTransactionFlags(tx); flags = Common.getTransactionFlags(tx, height);
} catch (e) { } catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : 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[] { static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
return txs.map(Common.classifyTransaction); return txs.map(tx => Common.classifyTransaction(tx, height));
} }
static stripTransaction(tx: TransactionExtended): TransactionStripped { static stripTransaction(tx: TransactionExtended): TransactionStripped {

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 81; private static currentVersion = 83;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@@ -700,6 +700,16 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"'); await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81); 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`); 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(); export default new DatabaseMigration();

View File

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

View File

@@ -1,6 +1,7 @@
import config from '../../config'; import config from '../../config';
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
class ChannelsRoutes { class ChannelsRoutes {
constructor() { } constructor() { }
@@ -22,7 +23,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search); const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels); res.json(channels);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel); res.json(channel);
} catch (e) { } 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 : ''; const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) { if (index < -1) {
res.status(400).send('Invalid index'); handleError(req, res, 400, 'Invalid index');
return; return;
} }
if (['open', 'active', 'closed'].includes(status) === false) { if (['open', 'active', 'closed'].includes(status) === false) {
res.status(400).send('Invalid status'); handleError(req, res, 400, 'Invalid status');
return; return;
} }
@@ -69,14 +70,14 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString()); res.header('X-Total-Count', channelsCount.toString());
res.json(channels); res.json(channels);
} catch (e) { } 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> { private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try { try {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
res.status(400).send('Not an array'); handleError(req, res, 400, 'Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
@@ -107,7 +108,7 @@ class ChannelsRoutes {
res.json(result); res.json(result);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } 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 nodesApi from './nodes.api';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import statisticsApi from './statistics.api'; import statisticsApi from './statistics.api';
import { handleError } from '../../utils/api';
class GeneralLightningRoutes { class GeneralLightningRoutes {
constructor() { } constructor() { }
@@ -27,7 +29,7 @@ class GeneralLightningRoutes {
channels: channels, channels: channels,
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } 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(); const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics); res.json(statistics);
} catch (e) { } 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 nodesApi from './nodes.api';
import DB from '../../database'; import DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces'; import { INodesRanking } from '../../mempool.interfaces';
import { handleError } from '../../utils/api';
class NodesRoutes { class NodesRoutes {
constructor() { } constructor() { }
@@ -31,7 +32,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes); res.json(nodes);
} catch (e) { } 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) {} } catch (e) {}
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes); res.json(nodes);
} catch (e) { } 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 { try {
const node = await nodesApi.$getNode(req.params.public_key); const node = await nodesApi.$getNode(req.params.public_key);
if (!node) { if (!node) {
res.status(404).send('Node not found'); handleError(req, res, 404, 'Node not found');
return; return;
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
@@ -203,7 +204,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } 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 { try {
const node = await nodesApi.$getFeeHistogram(req.params.public_key); const node = await nodesApi.$getFeeHistogram(req.params.public_key);
if (!node) { if (!node) {
res.status(404).send('Node not found'); handleError(req, res, 404, 'Node not found');
return; return;
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
@@ -231,7 +232,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } 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, topByChannels: topChannelsNodes,
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes); res.json(worldNodes);
} catch (e) { } 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) { 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; return;
} }
@@ -335,7 +336,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } 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) { 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; return;
} }
@@ -362,7 +363,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } 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 config from '../../config';
import elementsParser from './elements-parser'; import elementsParser from './elements-parser';
import icons from './icons'; import icons from './icons';
import { handleError } from '../../utils/api';
class LiquidRoutes { class LiquidRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@@ -42,7 +43,7 @@ class LiquidRoutes {
res.setHeader('content-length', result.length); res.setHeader('content-length', result.length);
res.send(result); res.send(result);
} else { } 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) { if (result) {
res.json(result); res.json(result);
} else { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs); res.json(pegs);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves); res.json(reserves);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply); res.json(currentSupply);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves); res.json(currentReserves);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus); res.json(auditStatus);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos); res.json(expiredUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs); res.json(recentPegs);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume); res.json(pegsVolume);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount); res.json(pegsCount);
} catch (e) { } 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; const lastBlockIndex = blocks.length - 1;
let hasBlockStack = blocks.length >= 8; let hasBlockStack = blocks.length >= 8;
let stackWeight; let stackWeight;
let feeStatsCalculator: OnlineFeeStatsCalculator | void; let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
if (hasBlockStack) { if (hasBlockStack) {
if (blockWeights && blockWeights[7] !== null) { if (blockWeights && blockWeights[7] !== null) {
stackWeight = blockWeights[7]; stackWeight = blockWeights[7];
@@ -380,28 +380,36 @@ class MempoolBlocks {
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); 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 cluster of clusters) {
for (const memberTxid of cluster) { for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid]; const mempoolTx = mempool[memberTxid];
if (mempoolTx) { if (mempoolTx) {
const ancestors: Ancestor[] = []; // ugly micro-optimization to avoid allocating new arrays
const descendants: Ancestor[] = []; ancestors.length = 0;
descendants.length = 0;
let matched = false; let matched = false;
cluster.forEach(txid => { cluster.forEach(txid => {
ancestor = mempool[txid];
if (txid === memberTxid) { if (txid === memberTxid) {
matched = true; matched = true;
} else { } else {
if (!mempool[txid]) { if (!ancestor) {
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]); console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
return;
} }
const relative = { const relative = {
txid: txid, txid: txid,
fee: mempool[txid].fee, fee: ancestor.fee,
weight: (mempool[txid].adjustedVsize * 4), weight: (ancestor.adjustedVsize * 4),
}; };
if (matched) { if (matched) {
descendants.push(relative); 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 { } else {
ancestors.push(relative); ancestors.push(relative);
} }
@@ -410,7 +418,20 @@ class MempoolBlocks {
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true; 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; const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results // update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended; 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 totalSize = 0;
let totalVsize = 0; let totalVsize = 0;
let totalWeight = 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) { if (txid) {
mempoolTx = mempool[txid]; mempoolTx = mempool[txid];
// save position in projected blocks // save position in projected blocks
@@ -445,30 +470,37 @@ class MempoolBlocks {
vsize: totalVsize + (mempoolTx.vsize / 2), vsize: totalVsize + (mempoolTx.vsize / 2),
}; };
const acceleration = accelerations[txid]; if (txid in accelerations) {
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { acceleration = accelerations[txid];
if (!mempoolTx.acceleration) { if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
mempoolTx.cpfpDirty = true; if (!mempoolTx.acceleration) {
} mempoolTx.cpfpDirty = true;
mempoolTx.acceleration = true; }
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; mempoolTx.acceleration = true;
mempoolTx.acceleratedAt = acceleration?.added; mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.feeDelta = acceleration?.feeDelta; mempoolTx.acceleratedAt = acceleration?.added;
for (const ancestor of mempoolTx.ancestors || []) { mempoolTx.feeDelta = acceleration?.feeDelta;
if (!mempool[ancestor.txid].acceleration) { for (const ancestor of mempoolTx.ancestors || []) {
mempool[ancestor.txid].cpfpDirty = true; 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 { } else {
if (mempoolTx.acceleration) { if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true; mempoolTx.cpfpDirty = true;
delete mempoolTx.acceleration;
} }
delete mempoolTx.acceleration;
} }
// online calculation of stack-of-blocks fee stats // online calculation of stack-of-blocks fee stats
@@ -486,7 +518,7 @@ class MempoolBlocks {
} }
} }
} }
return this.dataToMempoolBlocks( mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
block, block,
transactions, transactions,
totalSize, totalSize,
@@ -494,7 +526,7 @@ class MempoolBlocks {
totalFees, totalFees,
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined, (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
); );
}); };
if (saveResults) { if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);

View File

@@ -19,12 +19,13 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private mempoolCandidates: { [txid: string ]: boolean } = {}; private mempoolCandidates: { [txid: string ]: boolean } = {};
private spendMap = new Map<string, MempoolTransactionExtended>(); 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, 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 }; maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], 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[], 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 accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; 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; }, 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; this.mempoolChangedCallback = fn;
} }
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number, 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 { candidates?: GbtCandidates) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn; this.$asyncMempoolChangedCallback = fn;
} }
@@ -362,12 +363,15 @@ class Mempool {
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length; const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { this.recentlyDeleted.unshift(deletedTransactions);
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta); 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'); 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'); this.updateTimerProgress(timer, 'completed async mempool callback');
} }
@@ -541,16 +545,7 @@ class Mempool {
} }
} }
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void { public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): 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 {
for (const rbfTransaction in rbfTransactions) { for (const rbfTransaction in rbfTransactions) {
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) { if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
// Store replaced transactions // Store replaced transactions

View File

@@ -10,6 +10,7 @@ import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository'; import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration'; import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes { class MiningRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@@ -53,12 +54,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
const timestamp = parseInt(req.query.timestamp as string, 10) || 0; const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string; const currency = req.query.currency as string;
let response; let response;
if (timestamp && currency) { if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency); response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
@@ -71,7 +72,7 @@ class MiningRoutes {
} }
res.status(200).send(response); res.status(200).send(response);
} catch (e) { } 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); res.json(stats);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { 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 { } 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); res.json(poolBlocks);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { 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 { } 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); res.json(pools);
} }
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats); res.json(stats);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates); res.json(hashrates);
} catch (e) { } 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); res.json(hashrates);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { 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 { } 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) { private async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0; let currentHashrate = 0, currentDifficulty = 0;
try { try {
currentHashrate = await bitcoinClient.getNetworkHashPs(); currentHashrate = await bitcoinClient.getNetworkHashPs(1008);
currentDifficulty = await bitcoinClient.getDifficulty(); currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) { } catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
@@ -203,7 +204,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty, currentDifficulty: currentDifficulty,
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards); res.json(blockRewards);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates); res.json(blockFeeRates);
} catch (e) { } 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 weights: blockWeights
}); });
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) { } 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); const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) { if (!audit) {
res.status(204).send(`This block has not been audited.`); handleError(req, res, 204, `This block has not been audited.`);
return; return;
} }
@@ -335,7 +336,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit); res.json(audit);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result); res.json(result);
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) { } 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.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null'); res.json(audit || 'null');
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) { } 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.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { 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; return;
} }
res.status(200).send(accelerationApi.accelerations || []); res.status(200).send(accelerationApi.accelerations || []);
} catch (e) { } 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); accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send(); res.status(200).send();
} catch (e) { } 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, 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 { class RbfCache {
private replacedBy: Map<string, string> = new Map(); private replacedBy: Map<string, string> = new Map();
private replaces: 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); setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
} }
/**
* Low level cache operations
*/
private addTx(txid: string, tx: MempoolTransactionExtended): void { private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx); this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid }); this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
@@ -92,6 +112,12 @@ class RbfCache {
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid }); this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
} }
/**
* Basic data structure operations
* must uphold tree invariants
*/
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) { if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
return; return;
@@ -114,6 +140,10 @@ class RbfCache {
if (!replacedTx.rbf) { if (!replacedTx.rbf) {
txFullRbf = true; txFullRbf = true;
} }
if (this.replacedBy.has(replacedTx.txid)) {
// should never happen
continue;
}
this.replacedBy.set(replacedTx.txid, newTx.txid); this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) { if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid); const treeId = this.treeMap.get(replacedTx.txid);
@@ -140,18 +170,47 @@ class RbfCache {
} }
} }
newTx.fullRbf = txFullRbf; newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid;
const newTree = { const newTree = {
tx: newTx, tx: newTx,
time: newTime, time: newTime,
fullRbf: treeFullRbf, fullRbf: treeFullRbf,
replaces: replacedTrees replaces: replacedTrees
}; };
this.addTree(treeId, newTree); this.addTree(newTree.tx.txid, newTree);
this.updateTreeMap(treeId, newTree); this.updateTreeMap(newTree.tx.txid, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid)); 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 { public has(txId: string): boolean {
return this.txs.has(txId); return this.txs.has(txId);
} }
@@ -232,32 +291,6 @@ class RbfCache {
return changes; 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? // is the transaction involved in a full rbf replacement?
public isFullRbf(txid: string): boolean { public isFullRbf(txid: string): boolean {
const treeId = this.treeMap.get(txid); const treeId = this.treeMap.get(txid);
@@ -271,6 +304,10 @@ class RbfCache {
return tree?.fullRbf; return tree?.fullRbf;
} }
/**
* Cache maintenance & utility functions
*/
private cleanup(): void { private cleanup(): void {
const now = Date.now(); const now = Date.now();
for (const txid of this.expiring.keys()) { for (const txid of this.expiring.keys()) {
@@ -299,10 +336,6 @@ class RbfCache {
for (const tx of (replaces || [])) { for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache // recursively remove prior versions from the cache
this.replacedBy.delete(tx); 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); 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 { try {
txs.forEach(txEntry => { txs.forEach(txEntry => {
this.txs.set(txEntry.value.txid, txEntry.value); this.txs.set(txEntry.value.txid, txEntry.value);
}); });
this.staleCount = 0; this.staleCount = 0;
for (const deflatedTree of trees) { for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); 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 => { expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) { if (this.txs.has(expiringEntry.key)) {
@@ -385,6 +425,31 @@ class RbfCache {
} }
}); });
this.staleCount = 0; 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(); await this.checkTrees();
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
this.cleanup(); this.cleanup();
@@ -426,6 +491,12 @@ class RbfCache {
return; 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 // recursively reconstruct child trees
for (const childId of treeInfo.replaces) { for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
@@ -457,10 +528,6 @@ class RbfCache {
fullRbf: treeInfo.fullRbf, fullRbf: treeInfo.fullRbf,
replaces, replaces,
}; };
this.treeMap.set(txid, root);
if (root === txid) {
this.addTree(root, tree);
}
return tree; return tree;
} }
@@ -511,6 +578,7 @@ class RbfCache {
processTxs(txs); processTxs(txs);
} }
// evict missing transactions
for (const txid of txids) { for (const txid of txids) {
if (!found[txid]) { if (!found[txid]) {
this.evict(txid, false); this.evict(txid, false);

View File

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

View File

@@ -0,0 +1,26 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import WalletApi from './wallets';
class ServicesRoutes {
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
;
}
private async $getWallet(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString());
const walletId = req.params.walletId;
const wallet = await WalletApi.getWallet(walletId);
res.status(200).send(wallet);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new ServicesRoutes();

View File

@@ -0,0 +1,153 @@
import config from '../../config';
import logger from '../../logger';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import axios from 'axios';
import { TransactionExtended } from '../../mempool.interfaces';
interface WalletAddress {
address: string;
active: boolean;
stats: {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
};
transactions: IEsploraApi.AddressTxSummary[];
lastSync: number;
}
interface Wallet {
name: string;
addresses: Record<string, WalletAddress>;
lastPoll: number;
}
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
class WalletApi {
private wallets: Record<string, Wallet> = {};
private syncing = false;
constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
return acc;
}, {} as Record<string, Wallet>) : {};
}
public getWallet(wallet: string): Record<string, WalletAddress> {
return this.wallets?.[wallet]?.addresses || {};
}
// resync wallet addresses from the services backend
async $syncWallets(): Promise<void> {
if (!config.WALLETS.ENABLED || this.syncing) {
return;
}
this.syncing = true;
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
try {
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`);
const addresses: Record<string, WalletAddress> = response.data;
const addressList: WalletAddress[] = Object.values(addresses);
// sync all current addresses
for (const address of addressList) {
await this.$syncWalletAddress(wallet, address);
}
// remove old addresses
for (const address of Object.keys(wallet.addresses)) {
if (!addresses[address]) {
delete wallet.addresses[address];
}
}
wallet.lastPoll = Date.now();
logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`);
} catch (e) {
logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
this.syncing = false;
}
// resync address transactions from esplora
async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise<void> {
// fetch full transaction data if the address is new or still active and hasn't been synced in the last hour
const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000);
if (refreshTransactions) {
try {
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
const addressInfo = await bitcoinApi.$getAddress(address.address);
const walletAddress: WalletAddress = {
address: address.address,
active: address.active,
transactions: summary,
stats: addressInfo.chain_stats,
lastSync: Date.now(),
};
wallet.addresses[address.address] = walletAddress;
} catch (e) {
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
// check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, IEsploraApi.Transaction[]> {
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {};
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
walletTransactions[walletKey] = [];
for (const tx of blockTxs) {
const funded: Record<string, number> = {};
const spent: Record<string, number> = {};
const fundedCount: Record<string, number> = {};
const spentCount: Record<string, number> = {};
let anyMatch = false;
for (const vin of tx.vin) {
const address = vin.prevout?.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
spentCount[address] = (spentCount[address] ?? 0) + 1;
}
}
for (const vout of tx.vout) {
const address = vout.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
}
}
for (const address of Object.keys({ ...funded, ...spent })) {
// update address stats
wallet.addresses[address].stats.tx_count++;
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0;
wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0;
wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0;
wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0;
// add tx to summary
const txSummary: IEsploraApi.AddressTxSummary = {
txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: block.height,
time: block.timestamp,
};
wallet.addresses[address].transactions?.push(txSummary);
}
if (anyMatch) {
walletTransactions[walletKey].push(tx);
}
}
}
return walletTransactions;
}
}
export default new WalletApi();

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 adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
const feePerVbytes = (transaction.fee || 0) / fractionalVsize; const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
order: this.txidToOrdering(transaction.txid), order: this.txidToOrdering(transaction.txid),
vsize, vsize,
@@ -128,7 +129,7 @@ class TransactionUtils {
sigops, sigops,
feePerVsize: feePerVbytes, feePerVsize: feePerVbytes,
adjustedFeePerVsize: adjustedFeePerVsize, adjustedFeePerVsize: adjustedFeePerVsize,
effectiveFeePerVsize: adjustedFeePerVsize, effectiveFeePerVsize: effectiveFeePerVsize,
}); });
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) { if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
transactionExtended.firstSeen = Math.round((Date.now() / 1000)); transactionExtended.firstSeen = Math.round((Date.now() / 1000));
@@ -338,6 +339,87 @@ class TransactionUtils {
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript]; 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(); export default new TransactionUtils();

View File

@@ -16,6 +16,7 @@ import transactionUtils from './transaction-utils';
import rbfCache, { ReplacementInfo } from './rbf-cache'; import rbfCache, { ReplacementInfo } from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment'; import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api'; import feeApi from './fee-api';
import BlocksRepository from '../repositories/BlocksRepository';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import Audit from './audit'; import Audit from './audit';
@@ -26,6 +27,7 @@ import mempool from './mempool';
import statistics from './statistics/statistics'; import statistics from './statistics/statistics';
import accelerationRepository from '../repositories/AccelerationRepository'; import accelerationRepository from '../repositories/AccelerationRepository';
import bitcoinApi from './bitcoin/bitcoin-api-factory'; import bitcoinApi from './bitcoin/bitcoin-api-factory';
import walletApi from './services/wallets';
interface AddressTransactions { interface AddressTransactions {
mempool: MempoolTransactionExtended[], mempool: MempoolTransactionExtended[],
@@ -34,6 +36,7 @@ interface AddressTransactions {
} }
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp'; import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read';
// valid 'want' subscriptions // valid 'want' subscriptions
const wantable = [ const wantable = [
@@ -305,6 +308,14 @@ class WebsocketHandler {
} }
} }
if (parsedMessage && parsedMessage['track-wallet']) {
if (parsedMessage['track-wallet'] === 'stop') {
client['track-wallet'] = null;
} else {
client['track-wallet'] = parsedMessage['track-wallet'];
}
}
if (parsedMessage && parsedMessage['track-asset']) { if (parsedMessage && parsedMessage['track-asset']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
client['track-asset'] = parsedMessage['track-asset']; client['track-asset'] = parsedMessage['track-asset'];
@@ -520,8 +531,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, async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> { candidates?: GbtCandidates): Promise<void> {
if (!this.webSocketServers.length) { if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set'); throw new Error('No WebSocket.Server have been set');
@@ -529,6 +549,8 @@ class WebsocketHandler {
this.printLogs(); this.printLogs();
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool); const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
let added = newTransactions; let added = newTransactions;
let removed = deletedTransactions; let removed = deletedTransactions;
@@ -547,7 +569,7 @@ class WebsocketHandler {
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo(); const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations(); const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
@@ -578,7 +600,7 @@ class WebsocketHandler {
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
for (const tx of newTransactions) { for (const tx of newTransactions) {
if (rbfTransactions[tx.txid]) { 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 }); replacedTransactions.push({ replaced: replaced.txid, by: tx });
} }
} }
@@ -947,7 +969,7 @@ class WebsocketHandler {
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions); memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT && memPool.isInSync()) { if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
@@ -1017,6 +1039,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 } = {}; const confirmedTxids: { [txid: string]: boolean } = {};
// Update mempool to remove transactions included in the new block // Update mempool to remove transactions included in the new block
@@ -1091,6 +1121,9 @@ class WebsocketHandler {
replaced: replacedTransactions, replaced: replacedTransactions,
}; };
// check for wallet transactions
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
const responseCache = { ...this.socketData }; const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string { function getCachedResponse(key, data): string {
if (!responseCache[key]) { if (!responseCache[key]) {
@@ -1295,6 +1328,11 @@ class WebsocketHandler {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
} }
if (client['track-wallet']) {
const trackedWallet = client['track-wallet'];
response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {});
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }

View File

@@ -32,6 +32,7 @@ interface IConfig {
AUTOMATIC_POOLS_UPDATE: boolean; AUTOMATIC_POOLS_UPDATE: boolean;
POOLS_JSON_URL: string, POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string, POOLS_JSON_TREE_URL: string,
POOLS_UPDATE_DELAY: number,
AUDIT: boolean; AUDIT: boolean;
RUST_GBT: boolean; RUST_GBT: boolean;
LIMIT_GBT: boolean; LIMIT_GBT: boolean;
@@ -85,6 +86,7 @@ interface IConfig {
TIMEOUT: number; TIMEOUT: number;
COOKIE: boolean; COOKIE: boolean;
COOKIE_PATH: string; COOKIE_PATH: string;
DEBUG_LOG_PATH: string;
}; };
SECOND_CORE_RPC: { SECOND_CORE_RPC: {
HOST: string; HOST: string;
@@ -160,6 +162,10 @@ interface IConfig {
PAID: boolean; PAID: boolean;
API_KEY: string; API_KEY: string;
}, },
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
}
} }
const defaults: IConfig = { const defaults: IConfig = {
@@ -192,8 +198,9 @@ const defaults: IConfig = {
'AUTOMATIC_POOLS_UPDATE': false, 'AUTOMATIC_POOLS_UPDATE': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', '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_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, 'AUDIT': false,
'RUST_GBT': false, 'RUST_GBT': true,
'LIMIT_GBT': false, 'LIMIT_GBT': false,
'CPFP_INDEXING': false, 'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0, 'MAX_BLOCKS_BULK_QUERY': 0,
@@ -225,7 +232,8 @@ const defaults: IConfig = {
'PASSWORD': 'mempool', 'PASSWORD': 'mempool',
'TIMEOUT': 60000, 'TIMEOUT': 60000,
'COOKIE': false, 'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie' 'COOKIE_PATH': '/bitcoin/.cookie',
'DEBUG_LOG_PATH': '',
}, },
'SECOND_CORE_RPC': { 'SECOND_CORE_RPC': {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
@@ -320,6 +328,10 @@ const defaults: IConfig = {
'PAID': false, 'PAID': false,
'API_KEY': '', 'API_KEY': '',
}, },
'WALLETS': {
'ENABLED': false,
'WALLETS': [],
},
}; };
class Config implements IConfig { class Config implements IConfig {
@@ -341,6 +353,7 @@ class Config implements IConfig {
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES']; MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS']; REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE']; FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS'];
constructor() { constructor() {
const configs = this.merge(configFromFile, defaults); const configs = this.merge(configFromFile, defaults);
@@ -362,6 +375,7 @@ class Config implements IConfig {
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES; this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS; this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE; this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS;
} }
merge = (...objects: object[]): IConfig => { merge = (...objects: object[]): IConfig => {

View File

@@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes'; import miningRoutes from './api/mining/mining-routes';
import liquidRoutes from './api/liquid/liquid.routes'; import liquidRoutes from './api/liquid/liquid.routes';
import bitcoinRoutes from './api/bitcoin/bitcoin.routes'; import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
import servicesRoutes from './api/services/services-routes';
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher'; import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
import forensicsService from './tasks/lightning/forensics.service'; import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater'; import priceUpdater from './tasks/price-updater';
@@ -46,6 +47,7 @@ import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes'; import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes'; import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks'; import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@@ -211,6 +213,8 @@ class Server {
} }
}); });
} }
poolsUpdater.$startService();
} }
async runMainUpdateLoop(): Promise<void> { async runMainUpdateLoop(): Promise<void> {
@@ -236,6 +240,10 @@ class Server {
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate); await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
} }
indexer.$run(); indexer.$run();
if (config.WALLETS.ENABLED) {
// might take a while, so run in the background
walletApi.$syncWallets();
}
if (config.FIAT_PRICE.ENABLED) { if (config.FIAT_PRICE.ENABLED) {
priceUpdater.$run(); priceUpdater.$run();
} }
@@ -333,6 +341,9 @@ class Server {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) { if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app); accelerationRoutes.initRoutes(this.app);
} }
if (config.WALLETS.ENABLED) {
servicesRoutes.initRoutes(this.app);
}
if (!config.MEMPOOL.OFFICIAL) { if (!config.MEMPOOL.OFFICIAL) {
aboutRoutes.initRoutes(this.app); aboutRoutes.initRoutes(this.app);
} }

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

View File

@@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips';
import blocks from '../api/blocks'; import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository'; import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils'; import transactionUtils from '../api/transaction-utils';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
interface DatabaseBlock { interface DatabaseBlock {
id: string; id: string;
@@ -56,6 +57,7 @@ interface DatabaseBlock {
utxoSetChange: number; utxoSetChange: number;
utxoSetSize: number; utxoSetSize: number;
totalInputAmt: number; totalInputAmt: number;
firstSeen: number;
} }
const BLOCK_DB_FIELDS = ` const BLOCK_DB_FIELDS = `
@@ -98,7 +100,8 @@ const BLOCK_DB_FIELDS = `
blocks.header, blocks.header,
blocks.utxoset_change AS utxoSetChange, blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize, 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 { 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 * Convert a mysql row block into a BlockExtended. Note that you
* must provide the correct field into dbBlk object param * must provide the correct field into dbBlk object param
@@ -1054,6 +1075,7 @@ class BlocksRepository {
id: dbBlk.poolId, id: dbBlk.poolId,
name: dbBlk.poolName, name: dbBlk.poolName,
slug: dbBlk.poolSlug, slug: dbBlk.poolSlug,
minerNames: null,
}; };
extras.avgFee = dbBlk.avgFee; extras.avgFee = dbBlk.avgFee;
extras.avgFeeRate = dbBlk.avgFeeRate; extras.avgFeeRate = dbBlk.avgFeeRate;
@@ -1076,6 +1098,7 @@ class BlocksRepository {
extras.utxoSetSize = dbBlk.utxoSetSize; extras.utxoSetSize = dbBlk.utxoSetSize;
extras.totalInputAmt = dbBlk.totalInputAmt; extras.totalInputAmt = dbBlk.totalInputAmt;
extras.virtualSize = dbBlk.weight / 4.0; extras.virtualSize = dbBlk.weight / 4.0;
extras.firstSeen = dbBlk.firstSeen;
// Re-org can happen after indexing so we need to always get the // Re-org can happen after indexing so we need to always get the
// latest state from core // latest state from core
@@ -1106,7 +1129,7 @@ class BlocksRepository {
let summaryVersion = 0; let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx)); 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; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC
@@ -1123,6 +1146,10 @@ class BlocksRepository {
} }
} }
if (extras.pool.name === 'OCEAN') {
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
}
blk.extras = <BlockExtension>extras; blk.extras = <BlockExtension>extras;
return <BlockExtended>blk; return <BlockExtended>blk;
} }

View File

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

View File

@@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info';
import logger from '../logger'; import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent'; import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https'; import * as https from 'https';
import { Common } from '../api/common';
/** /**
* Maintain the most recent version of pools-v2.json * Maintain the most recent version of pools-v2.json
*/ */
class PoolsUpdater { class PoolsUpdater {
tag = 'PoolsUpdater';
lastRun: number = 0; lastRun: number = 0;
currentSha: string | null = null; currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_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> { public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false config.MEMPOOL.ENABLED === false
@@ -23,11 +37,8 @@ class PoolsUpdater {
return; return;
} }
const oneWeek = 604800;
const oneDay = 86400;
const now = new Date().getTime() / 1000; 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; return;
} }
@@ -43,7 +54,7 @@ class PoolsUpdater {
this.currentSha = await this.getShaFromDb(); 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) { if (this.currentSha !== null && this.currentSha === githubSha) {
return; return;
} }
@@ -53,16 +64,16 @@ class PoolsUpdater {
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool !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.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`); 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; return;
} }
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === null) { 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 { } 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); const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) { if (poolsJson === undefined) {
@@ -71,7 +82,7 @@ class PoolsUpdater {
poolsParser.setMiningPools(poolsJson); poolsParser.setMiningPools(poolsJson);
if (config.DATABASE.ENABLED === false) { // Don't run db operations 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; return;
} }
@@ -81,14 +92,14 @@ class PoolsUpdater {
await this.updateDBSha(githubSha); await this.updateDBSha(githubSha);
await DB.query('COMMIT;'); await DB.query('COMMIT;');
} catch (e) { } 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;'); 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) { } catch (e) {
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week this.lastRun = now - 600; // Try again in 10 minutes
logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining); 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('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) { } 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"'); const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null); return (rows.length > 0 ? rows[0].string : null);
} catch (e) { } 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; 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; return null;
} }
@@ -186,7 +197,7 @@ class PoolsUpdater {
} }
return data.data; return data.data;
} catch (e) { } 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++; retry++;
} }
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL); 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) { if (!opN) {
return; return;
} }
if (!opN.startsWith('OP_PUSHNUM_')) { if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); 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) { if (!opM) {
return; return;
} }
if (!opM.startsWith('OP_PUSHNUM_')) { if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
@@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number {
} else { } else {
return 9; 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, "AUTOMATIC_POOLS_UPDATE": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "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_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"POOLS_UPDATE_DELAY": 604800,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0, "MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6, "DISK_CACHE_BLOCK_INTERVAL": 6,
@@ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_AUTOMATIC_POOLS_UPDATE: "" MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
MEMPOOL_POOLS_JSON_URL: "" MEMPOOL_POOLS_JSON_URL: ""
MEMPOOL_POOLS_JSON_TREE_URL: "" MEMPOOL_POOLS_JSON_TREE_URL: ""
MEMPOOL_POOLS_UPDATE_DELAY: ""
MEMPOOL_CPFP_INDEXING: "" MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: "" MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: "" MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""

View File

@@ -36,6 +36,7 @@
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__, "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__ "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
}, },
@@ -46,7 +47,8 @@
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__, "TIMEOUT": __CORE_RPC_TIMEOUT__,
"COOKIE": __CORE_RPC_COOKIE__, "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": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "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_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_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_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_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_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0} __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_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false} __CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""} __CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
# ELECTRUM # ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} __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_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_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_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_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!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 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_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__!${__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_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_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!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 ### 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: 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 ### 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: Build the frontend:

View File

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

View File

@@ -0,0 +1,48 @@
{
"theme": "wiz",
"enterprise": "bitb",
"branding": {
"name": "bitb",
"title": "BITB",
"site_id": 20,
"header_img": "/resources/bitblogo.svg",
"footer_img": "/resources/bitblogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "BITB"
}
},
{
"component": "goggles",
"mobileOrder": 5
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "BITB",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "BITB"
}
}
]
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,15 +1,15 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from './app.preloading-strategy' import { AppPreloadingStrategy } from '@app/app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component'; import { BlockViewComponent } from '@components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component'; import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component'; import { ClockComponent } from '@components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component'; import { StatusViewComponent } from '@components/status-view/status-view.component';
import { AddressGroupComponent } from './components/address-group/address-group.component'; import { AddressGroupComponent } from '@components/address-group/address-group.component';
import { TrackerComponent } from './components/tracker/tracker.component'; import { TrackerComponent } from '@components/tracker/tracker.component';
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component'; import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from './route-guards'; import { TrackerGuard } from '@app/route-guards';
const browserWindow = window || {}; const browserWindow = window || {};
// @ts-ignore // @ts-ignore
@@ -22,16 +22,16 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@@ -45,7 +45,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@@ -60,12 +60,12 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@@ -83,7 +83,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@@ -103,16 +103,16 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@@ -126,7 +126,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@@ -138,22 +138,22 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'tx', path: 'tx',
canMatch: [TrackerGuard], canMatch: [TrackerGuard],
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule), loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule),
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@@ -165,19 +165,19 @@ let routes: Routes = [
children: [ children: [
{ {
path: '', path: '',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'testnet', path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'testnet4', path: 'testnet4',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'signet', path: 'signet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
], ],
}, },
@@ -212,7 +212,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
]; ];
@@ -225,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@@ -248,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
}, },
{ {
path: '', path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@@ -260,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@@ -281,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
children: [ children: [
{ {
path: '', path: '',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'testnet', path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
], ],
}, },
@@ -296,7 +296,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
}, },
{ {
path: '', path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
]; ];

View File

@@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server'; import { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from './injection-tokens'; import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppModule } from './app.module'; import { AppModule } from '@app/app.module';
import { AppComponent } from './components/app/app.component'; import { AppComponent } from '@components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor'; import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { ZoneService } from './services/zone.service'; import { ZoneService } from '@app/services/zone.service';
@NgModule({ @NgModule({
@@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service';
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppServerModule {} export class AppServerModule {}

View File

@@ -2,35 +2,38 @@ import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core'; import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ZONE_SERVICE } from './injection-tokens'; import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from '@app/app-routing.module';
import { AppComponent } from './components/app/app.component'; import { AppComponent } from '@components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { StateService } from './services/state.service'; import { OrdApiService } from '@app/services/ord-api.service';
import { CacheService } from './services/cache.service'; import { StateService } from '@app/services/state.service';
import { PriceService } from './services/price.service'; import { CacheService } from '@app/services/cache.service';
import { EnterpriseService } from './services/enterprise.service'; import { PriceService } from '@app/services/price.service';
import { WebsocketService } from './services/websocket.service'; import { EnterpriseService } from '@app/services/enterprise.service';
import { AudioService } from './services/audio.service'; import { WebsocketService } from '@app/services/websocket.service';
import { PreloadService } from './services/preload.service'; import { AudioService } from '@app/services/audio.service';
import { SeoService } from './services/seo.service'; import { PreloadService } from '@app/services/preload.service';
import { OpenGraphService } from './services/opengraph.service'; import { SeoService } from '@app/services/seo.service';
import { ZoneService } from './services/zone-shim.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { SharedModule } from './shared/shared.module'; import { ZoneService } from '@app/services/zone-shim.service';
import { StorageService } from './services/storage.service'; import { SharedModule } from '@app/shared/shared.module';
import { HttpCacheInterceptor } from './services/http-cache.interceptor'; import { StorageService } from '@app/services/storage.service';
import { LanguageService } from './services/language.service'; import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { ThemeService } from './services/theme.service'; import { LanguageService } from '@app/services/language.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe'; import { ThemeService } from '@app/services/theme.service';
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe'; import { TimeService } from '@app/services/time.service';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy'; import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { ServicesApiServices } from './services/services-api.service'; import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
import { ServicesApiServices } from '@app/services/services-api.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
const providers = [ const providers = [
ElectrsApiService, ElectrsApiService,
OrdApiService,
StateService, StateService,
CacheService, CacheService,
PriceService, PriceService,
@@ -42,6 +45,7 @@ const providers = [
EnterpriseService, EnterpriseService,
LanguageService, LanguageService,
ThemeService, ThemeService,
TimeService,
ShortenStringPipe, ShortenStringPipe,
FiatShortenerPipe, FiatShortenerPipe,
FiatCurrencyPipe, FiatCurrencyPipe,

View File

@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { MasterPageComponent } from './components/master-page/master-page.component'; import { MasterPageComponent } from '@components/master-page/master-page.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: MasterPageComponent, component: MasterPageComponent,
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule), loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule),
data: { preload: true }, data: { preload: true },
} }
]; ];

View File

@@ -1,5 +1,5 @@
import { Transaction, Vin } from './interfaces/electrs.interface'; import { Transaction, Vin } from '@interfaces/electrs.interface';
import { Hash } from './shared/sha256'; import { Hash } from '@app/shared/sha256';
const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
@@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
return; return;
} }
const opN = ops.pop(); const opN = ops.pop();
if (!opN.startsWith('OP_PUSHNUM_')) { if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const n = parseInt(opN.match(/[0-9]+/)[0], 10); 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(); const opM = ops.pop();
if (!opM.startsWith('OP_PUSHNUM_')) { if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const m = parseInt(opM.match(/[0-9]+/)[0], 10); const m = parseInt(opM.match(/[0-9]+/)[0], 10);
@@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise<string> {
return hashArray return hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0')) .map((bytes) => bytes.toString(16).padStart(2, '0'))
.join(''); .join('');
} }

View File

@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { EnterpriseService } from '../../services/enterprise.service'; import { EnterpriseService } from '@app/services/enterprise.service';
@Component({ @Component({
selector: 'app-about-sponsors', selector: 'app-about-sponsors',

View File

@@ -53,7 +53,7 @@
<span>Spiral</span> <span>Spiral</span>
</a> </a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry"> <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> <defs>
<style> <style>
.d { .d {
@@ -125,17 +125,14 @@
<span>Blockstream</span> <span>Blockstream</span>
</a> </a>
<a href="https://unchained.com/" target="_blank" title="Unchained"> <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> <span>Unchained</span>
</a> </a>
<a href="https://gemini.com/" target="_blank" title="Gemini"> <a href="https://bitkey.world/" target="_blank" title="Bitkey">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image"> <img class="image" src="/resources/profile/bitkey.svg" />
<rect style="fill: black" width="360" height="360" /> <span>Bitkey</span>
<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>
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin"> <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"> <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> <span>Bull Bitcoin</span>
</a> </a>
<a href="https://exodus.com/" target="_blank" title="Exodus"> <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"/> <circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)"> <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)"/> <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> </svg>
<span>Exodus</span> <span>Exodus</span>
</a> </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>
</div> </div>
<ng-container> <ng-container>
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor"> <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> <h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper"> <div class="wrapper">
<ng-container> <ng-container>
@@ -435,7 +450,7 @@
Trademark Notice<br> Trademark Notice<br>
</div> </div>
<p> <p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, 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>
<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;. 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 { .image.not-rounded {
border-radius: 0; border-radius: 0;
width: 60px;
height: 60px;
} }
.intro { .intro {
@@ -94,6 +92,13 @@
} }
} }
.whale-sponsor {
img {
width: 70px;
height: 70px;
}
}
.alliances { .alliances {
margin-bottom: 100px; margin-bottom: 100px;
a { a {
@@ -158,9 +163,8 @@
margin: 40px 29px 10px; margin: 40px 29px 10px;
&.image.coldcard { &.image.coldcard {
border-radius: 0; border-radius: 0;
width: auto; height: auto;
max-height: 50px; margin: 20px 29px 20px;
margin: 40px 29px 14px 29px;
} }
} }
} }
@@ -254,3 +258,12 @@
width: 64px; width: 64px;
height: 64px; height: 64px;
} }
.enterprise-sponsor {
.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
}
}

View File

@@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { IBackendInfo } from '../../interfaces/websocket.interface'; import { IBackendInfo } from '@interfaces/websocket.interface';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { map, share, tap } from 'rxjs/operators'; import { map, share, tap } from 'rxjs/operators';
import { ITranslators } from '../../interfaces/node-api.interface'; import { ITranslators } from '@interfaces/node-api.interface';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { EnterpriseService } from '../../services/enterprise.service'; import { EnterpriseService } from '@app/services/enterprise.service';
@Component({ @Component({
selector: 'app-about', selector: 'app-about',

View File

@@ -1,9 +1,9 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about.component'; import { AboutComponent } from '@components/about/about.component';
import { AboutSponsorsComponent } from './about-sponsors.component'; import { AboutSponsorsComponent } from '@components/about/about-sponsors.component';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '@app/shared/shared.module';
const routes: Routes = [ const routes: Routes = [
{ {

View File

@@ -1,16 +1,16 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { md5, insecureRandomUUID } from '../../shared/common.utils'; import { md5, insecureRandomUUID } from '@app/shared/common.utils';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ETA, EtaService } from '../../services/eta.service'; import { ETA, EtaService } from '@app/services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '@interfaces/electrs.interface';
import { MiningStats } from '../../services/mining.service'; import { MiningStats } from '@app/services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service'; import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service'; import { EnterpriseService } from '@app/services/enterprise.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { isDevMode } from '@angular/core'; import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
@@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Output() changeMode = new EventEmitter<boolean>(); @Output() changeMode = new EventEmitter<boolean>();
calculating = true; calculating = true;
processing = false;
selectedOption: 'wait' | 'accel'; selectedOption: 'wait' | 'accel';
cantPayReason = ''; cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data quoteError = ''; // error fetching estimate or initial data
@@ -83,13 +84,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
timePaid: number = 0; // time acceleration requested timePaid: number = 0; // time acceleration requested
math = Math; math = Math;
isMobile: boolean = window.innerWidth <= 767.98; isMobile: boolean = window.innerWidth <= 767.98;
isProdDomain = ['mempool.space', isProdDomain = false;
'mempool-staging.va1.mempool.space',
'mempool-staging.fmt.mempool.space',
'mempool-staging.fra.mempool.space',
'mempool-staging.tk7.mempool.space',
'mempool-staging.sg1.mempool.space'
].indexOf(document.location.hostname) > -1;
private _step: CheckoutStep = 'summary'; private _step: CheckoutStep = 'summary';
simpleMode: boolean = true; simpleMode: boolean = true;
@@ -142,6 +137,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private authService: AuthServiceMempool, private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService, private enterpriseService: EnterpriseService,
) { ) {
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
this.accelerationUUID = insecureRandomUUID(); this.accelerationUUID = insecureRandomUUID();
// Check if Apple Pay available // Check if Apple Pay available
@@ -196,9 +192,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (changes.scrollEvent && this.scrollEvent) { if (changes.scrollEvent && this.scrollEvent) {
this.scrollToElement('acceleratePreviewAnchor', 'start'); this.scrollToElement('acceleratePreviewAnchor', 'start');
} }
if (changes.accelerating) { if (changes.accelerating && this.accelerating) {
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) { if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success'); this.moveToStep('success');
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
} }
} }
} }
@@ -371,6 +369,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.selectFeeRateIndex = index; this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee); this.userBid = Math.max(0, fee);
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.validateChoice();
} }
} }
@@ -378,9 +377,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* Account-based acceleration request * Account-based acceleration request
*/ */
accelerateWithMempoolAccount(): void { accelerateWithMempoolAccount(): void {
if (!this.canPay || this.calculating) { if (!this.canPay || this.calculating || this.processing) {
return; return;
} }
this.processing = true;
if (this.accelerationSubscription) { if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe(); this.accelerationSubscription.unsubscribe();
} }
@@ -390,6 +390,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationUUID this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true; this.showSuccess = true;
@@ -397,6 +398,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.moveToStep('paid'); this.moveToStep('paid');
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
} }
}); });
@@ -466,10 +468,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* APPLE PAY * APPLE PAY
*/ */
async requestApplePayPayment(): Promise<void> { async requestApplePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@@ -494,6 +500,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
console.error(`Unable to find apple pay button id='apple-pay-button'`); console.error(`Unable to find apple pay button id='apple-pay-button'`);
// Try again // Try again
setTimeout(this.requestApplePayPayment.bind(this), 500); setTimeout(this.requestApplePayPayment.bind(this), 500);
this.processing = false;
return; return;
} }
this.loadingApplePay = false; this.loadingApplePay = false;
@@ -505,6 +512,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`); console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details'; this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return; return;
} }
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
@@ -513,9 +521,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
cardTag, cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID this.accelerationUUID,
costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) { if (this.applePay) {
@@ -526,6 +536,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {
@@ -537,6 +548,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
}); });
} else { } else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) { if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify( errorMessage += ` and errors: ${JSON.stringify(
@@ -547,6 +559,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
}); });
} catch (e) { } catch (e) {
this.processing = false;
console.error(e); console.error(e);
} }
} }
@@ -557,10 +570,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* GOOGLE PAY * GOOGLE PAY
*/ */
async requestGooglePayPayment(): Promise<void> { async requestGooglePayPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@@ -595,6 +612,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`); console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details'; this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return; return;
} }
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
@@ -603,9 +621,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
cardTag, cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID this.accelerationUUID,
costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) { if (this.googlePay) {
@@ -616,6 +636,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {
@@ -627,6 +648,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
}); });
} else { } else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) { if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify( errorMessage += ` and errors: ${JSON.stringify(
@@ -644,10 +666,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
* CASHAPP * CASHAPP
*/ */
async requestCashAppPayment(): Promise<void> { async requestCashAppPayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) { if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe(); this.conversionsSubscription.unsubscribe();
} }
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => { async (conversions) => {
this.conversions = conversions; this.conversions = conversions;
@@ -678,6 +704,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.addEventListener('ontokenization', event => { this.cashAppPay.addEventListener('ontokenization', event => {
const { tokenResult, error } = event.detail; const { tokenResult, error } = event.detail;
if (error) { if (error) {
this.processing = false;
this.accelerateError = error; this.accelerateError = error;
} else if (tokenResult.status === 'OK') { } else if (tokenResult.status === 'OK') {
this.servicesApiService.accelerateWithCashApp$( this.servicesApiService.accelerateWithCashApp$(
@@ -685,9 +712,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId, tokenResult.details.cashAppPay.referenceId,
this.accelerationUUID this.accelerationUUID,
costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
if (this.cashAppPay) { if (this.cashAppPay) {
@@ -702,6 +731,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {

View File

@@ -12,7 +12,7 @@
</p> </p>
</div> </div>
<div class="spacer"></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 class="spacer"></div> <div class="spacer"></div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '@interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component'; import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component';
interface GraphBar { interface GraphBar {
rate: number; rate: number;

View File

@@ -1,11 +1,11 @@
<div <div
#tooltip #tooltip
*ngIf="accelerationInfo && tooltipPosition !== null"
class="acceleration-tooltip" class="acceleration-tooltip"
[style.left]="tooltipPosition.x + 'px'" [style.visibility]="accelerationInfo ? 'visible' : 'hidden'"
[style.top]="tooltipPosition.y + 'px'" [style.left]="tooltipPosition?.x + 'px'"
[style.top]="tooltipPosition?.y + 'px'"
> >
<table> <table *ngIf="accelerationInfo">
<tbody> <tbody>
<tr> <tr>
<td class="label" i18n="transaction.status|Transaction Status">Status</td> <td class="label" i18n="transaction.status|Transaction Status">Status</td>
@@ -21,14 +21,14 @@
</tr> </tr>
<tr *ngIf="accelerationInfo.fee"> <tr *ngIf="accelerationInfo.fee">
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td> <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>
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta"> <tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td> <td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
@if (accelerationInfo.status === 'accelerated') { @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 { } @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>
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight"> <tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
@@ -47,13 +47,13 @@
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()"> <tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td> <td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td class="value" *ngIf="accelerationInfo.pools"> <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]" <img *ngIf="accelerationInfo.poolsData[pool]"
class="pool-logo" class="pool-logo"
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'" [style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'" [src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'" onError="this.src = '/resources/mining-pools/default.svg'">
[alt]="'Logo of ' + pool.name + ' mining pool'"> <br *ngIf="i % 6 === 5">
</ng-container> </ng-container>
</td> </td>
</tr> </tr>

View File

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

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core'; import { Component, ElementRef, ViewChild, Input, OnChanges, HostListener } from '@angular/core';
@Component({ @Component({
selector: 'app-acceleration-timeline-tooltip', selector: 'app-acceleration-timeline-tooltip',
@@ -10,6 +10,7 @@ export class AccelerationTimelineTooltipComponent implements OnChanges {
@Input() cursorPosition: { x: number, y: number }; @Input() cursorPosition: { x: number, y: number };
tooltipPosition: any = null; tooltipPosition: any = null;
yScroll = window.scrollY;
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>; @ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
@@ -21,6 +22,9 @@ export class AccelerationTimelineTooltipComponent implements OnChanges {
let y = changes.cursorPosition.currentValue.y + 20; let y = changes.cursorPosition.currentValue.y + 20;
if (this.tooltipElement) { if (this.tooltipElement) {
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect(); const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
if (this.accelerationInfo?.status !== 'seen') {
elementBounds.width = 370; // ugly hack to handle varying width due to pools logo loading
}
if ((x + elementBounds.width) > (window.innerWidth - 10)) { if ((x + elementBounds.width) > (window.innerWidth - 10)) {
x = Math.max(0, window.innerWidth - elementBounds.width - 10); x = Math.max(0, window.innerWidth - elementBounds.width - 10);
} }
@@ -35,4 +39,12 @@ export class AccelerationTimelineTooltipComponent implements OnChanges {
hasPoolsData(): boolean { hasPoolsData(): boolean {
return Object.keys(this.accelerationInfo.poolsData).length > 0; return Object.keys(this.accelerationInfo.poolsData).length > 0;
} }
@HostListener('window:scroll', ['$event'])
onWindowScroll(): void {
if (this.tooltipPosition) {
this.tooltipPosition.y = this.tooltipPosition.y - (window.scrollY - this.yScroll);
}
this.yScroll = window.scrollY;
}
} }

View File

@@ -9,7 +9,7 @@
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (eta) { @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>
</div> </div>
@@ -38,7 +38,7 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
<app-time [time]="acceleratedAt - transactionTime"></app-time> <app-time [time]="firstSeenToAccelerated"></app-time>
</div> </div>
</div> </div>
<div class="node-spacer"></div> <div class="node-spacer"></div>
@@ -46,10 +46,8 @@
<div class="interval-time"> <div class="interval-time">
@if (tx.status.confirmed) { @if (tx.status.confirmed) {
<div class="interval-time"> <div class="interval-time">
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time> <app-time [time]="acceleratedToMined"></app-time>
</div> </div>
} @else if (standardETA && !tx.status.confirmed) {
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
} }
</div> </div>
</div> </div>
@@ -58,8 +56,8 @@
<div class="nodes"> <div class="nodes">
<div class="node" [id]="'first-seen'"> <div class="node" [id]="'first-seen'">
<div class="seen-to-acc right"></div> <div class="seen-to-acc right"></div>
<div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);"> <div class="shape-border hovering">
<div class="shape"></div> <div id="step" class="shape" (pointerover)="onHover($event, 'seen');"></div>
</div> </div>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div> <div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
<div class="time"> <div class="time">
@@ -80,8 +78,8 @@
} @else { } @else {
<div class="seen-to-acc right"></div> <div class="seen-to-acc right"></div>
} }
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);"> <div class="shape-border hovering">
<div class="shape"></div> <div id="step" class="shape" (pointerover)="onHover($event, 'accelerated');"></div>
@if (!tx.status.confirmed) { @if (!tx.status.confirmed) {
<div class="connector down loading"></div> <div class="connector down loading"></div>
} }
@@ -113,11 +111,8 @@
} @else { } @else {
<div class="seen-to-acc left"></div> <div class="seen-to-acc left"></div>
} }
<div class="shape-border" <div class="shape-border" [ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}">
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}" <div id="step" class="shape" (pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)" ></div>
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
(pointerout)="onBlur($event);">
<div class="shape"></div>
</div> </div>
@if (tx.status.confirmed) { @if (tx.status.confirmed) {
<div class="status"><span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span></div> <div class="status"><span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span></div>

View File

@@ -1,8 +1,8 @@
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core'; import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
import { ETA } from '../../services/eta.service'; import { ETA } from '@app/services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '@interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface'; import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '@app/services/mining.service';
@Component({ @Component({
selector: 'app-acceleration-timeline', selector: 'app-acceleration-timeline',
@@ -11,19 +11,16 @@ import { MiningService } from '../../services/mining.service';
}) })
export class AccelerationTimelineComponent implements OnInit, OnChanges { export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number; @Input() transactionTime: number;
@Input() acceleratedAt: number;
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() accelerationInfo: Acceleration; @Input() accelerationInfo: Acceleration;
@Input() eta: ETA; @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; now: number;
accelerateRatio: number; accelerateRatio: number;
useAbsoluteTime: boolean = false; useAbsoluteTime: boolean = false;
interval: number; firstSeenToAccelerated: number;
acceleratedToMined: number;
tooltipPosition = null; tooltipPosition = null;
hoverInfo: any = null; hoverInfo: any = null;
@@ -34,41 +31,28 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000; this.updateTimes();
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
this.miningService.getPools().subscribe(pools => { this.miningService.getPools().subscribe(pools => {
for (const pool of pools) { for (const pool of pools) {
this.poolsData[pool.unique_id] = pool; 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 { ngOnChanges(changes): void {
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65 this.updateTimes();
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
// if (changes?.eta?.currentValue) {
// if (changes?.acceleratedETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
// } else if (changes?.standardETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
// }
// }
// }
} }
ngOnDestroy(): void { updateTimes(): void {
clearInterval(this.interval); 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 { onHover(event, status: string): void {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
if (status === 'seen') { if (status === 'seen') {
this.hoverInfo = { this.hoverInfo = {
status, status,
@@ -97,12 +81,19 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
} }
} }
onBlur(event): void {
this.hoverInfo = null;
}
@HostListener('pointermove', ['$event']) @HostListener('pointermove', ['$event'])
onPointerMove(event) { onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY }; if (event.target.id === 'step') {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
} else {
this.hoverInfo = null;
}
}
@HostListener('document:click', ['$event'])
clickAway(event) {
if (event.target.id !== 'step') {
this.hoverInfo = null;
}
} }
} }

View File

@@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { EChartsOption } from '../../../graphs/echarts'; import { EChartsOption } from '@app/graphs/echarts';
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs'; import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
import { startWith, switchMap, tap } from 'rxjs/operators'; import { startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils'; import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
import { StorageService } from '../../../services/storage.service'; import { StorageService } from '@app/services/storage.service';
import { MiningService } from '../../../services/mining.service'; import { MiningService } from '@app/services/mining.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface'; import { Acceleration } from '@interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
@Component({ @Component({
selector: 'app-acceleration-fees-graph', selector: 'app-acceleration-fees-graph',
@@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
type: 'bar', type: 'bar',
barWidth: '90%', barWidth: '90%',
large: true, large: true,
barMinHeight: 1, barMinHeight: 3,
}, },
], ],
dataZoom: (this.widget || data.length === 0 )? undefined : [{ dataZoom: (this.widget || data.length === 0 )? undefined : [{

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ServicesApiServices } from '../../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
export type AccelerationStats = { export type AccelerationStats = {
totalRequested: number; totalRequested: number;

View File

@@ -33,7 +33,7 @@
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate> <app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
</td> </td>
<td class="bid text-right"> <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>
<td class="time text-right"> <td class="time text-right">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time> <app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@@ -41,7 +41,7 @@
</ng-container> </ng-container>
<ng-container *ngIf="!pending"> <ng-container *ngIf="!pending">
<td *ngIf="acceleration.boost != null" class="fee text-right"> <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>
<td *ngIf="acceleration.boost == null" class="fee text-right"> <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 === '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="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('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>
<td class="date text-right" *ngIf="!this.widget"> <td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time> <app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>

View File

@@ -1,12 +1,12 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core'; 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 { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { ServicesApiServices } from '../../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MiningService } from '../../../services/mining.service'; import { MiningService } from '@app/services/mining.service';
@Component({ @Component({
selector: 'app-accelerations-list', selector: 'app-accelerations-list',
@@ -61,8 +61,11 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.websocketService.want(['blocks']); this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`); this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
this.paramSubscription = this.route.params.pipe( this.paramSubscription = combineLatest([
tap(params => { this.route.params,
timer(0),
]).pipe(
tap(([params]) => {
this.page = +params['page'] || 1; this.page = +params['page'] || 1;
this.pageSubject.next(this.page); this.pageSubject.next(this.page);
}) })
@@ -148,4 +151,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.paramSubscription?.unsubscribe(); this.paramSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe(); this.keyNavigationSubscription?.unsubscribe();
} }
} }

View File

@@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { Acceleration, BlockExtended } from '@interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs'; import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
import { Color } from '../../block-overview-graph/sprite-types'; import { Color } from '@components/block-overview-graph/sprite-types';
import { hexToColor } from '../../block-overview-graph/utils'; import { hexToColor } from '@components/block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view'; import TxView from '@components/block-overview-graph/tx-view';
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants'; import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
import { ServicesApiServices } from '../../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { detectWebGL } from '../../../shared/graphs.utils'; import { detectWebGL } from '@app/shared/graphs.utils';
import { AudioService } from '../../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ThemeService } from '../../../services/theme.service'; import { ThemeService } from '@app/services/theme.service';
const acceleratedColor: Color = hexToColor('8F5FF6'); const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F')); const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));

View File

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

View File

@@ -1,8 +1,8 @@
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 { Transaction } from '@interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface'; import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts'; import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts';
import { MiningStats } from '../../../services/mining.service'; import { MiningStats } from '@app/services/mining.service';
function lighten(color, p): { r, g, b } { function lighten(color, p): { r, g, b } {
return { return {
@@ -23,7 +23,8 @@ function toRGB({r,g,b}): string {
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ActiveAccelerationBox implements OnChanges { export class ActiveAccelerationBox implements OnChanges {
@Input() tx: Transaction; @Input() acceleratedBy?: number[];
@Input() effectiveFeeRate?: number;
@Input() accelerationInfo: Acceleration; @Input() accelerationInfo: Acceleration;
@Input() miningStats: MiningStats; @Input() miningStats: MiningStats;
@Input() pools: number[]; @Input() pools: number[];
@@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges {
timespan = ''; timespan = '';
chartInstance: any = undefined; chartInstance: any = undefined;
constructor() {} constructor(
private cd: ChangeDetectorRef,
) {}
ngOnChanges(changes: SimpleChanges): void { 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) { if (pools && this.miningStats) {
this.prepareChartOptions(pools); this.prepareChartOptions(pools);
} }
@@ -67,17 +70,27 @@ export class ActiveAccelerationBox implements OnChanges {
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate); 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 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) => { acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId]; const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
let color = 'white';
if (index >= firstSignificantPool) {
if (numSignificantPools > 1) {
color = toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / Math.max((numSignificantPools - 1), 1)));
} else {
color = toRGB({ r: 147, g: 57, b: 244 });
}
}
data.push(getDataItem( data.push(getDataItem(
pool.lastEstimatedHashrate, pool.lastEstimatedHashrate,
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)), color,
`<b style="color: white">${pool.name} (${poolShare}%)</b>`, `<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true, true,
) as PieSeriesOption); ) as PieSeriesOption);
}) });
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%'; this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%'; const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
data.push(getDataItem( data.push(getDataItem(
@@ -128,6 +141,7 @@ export class ActiveAccelerationBox implements OnChanges {
} }
] ]
}; };
this.cd.markForCheck();
} }
onChartInit(ec) { onChartInit(ec) {
@@ -140,4 +154,4 @@ export class ActiveAccelerationBox implements OnChanges {
onToggleCpfp(): void { onToggleCpfp(): void {
this.toggleCpfp.emit(); this.toggleCpfp.emit();
} }
} }

View File

@@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { Acceleration } from '../../../interfaces/node-api.interface'; import { Acceleration } from '@interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
@Component({ @Component({
selector: 'app-pending-stats', selector: 'app-pending-stats',

View File

@@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts'; import { echarts, EChartsOption } from '@app/graphs/echarts';
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface'; import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { PriceService } from '../../services/price.service'; import { PriceService } from '@app/services/price.service';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
const periodSeconds = { const periodSeconds = {
'1d': (60 * 60 * 24), '1d': (60 * 60 * 24),
@@ -83,7 +83,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true; this.isLoading = true;
if (!this.address || !this.stats) { if (!this.addressSummary$ && (!this.address || !this.stats)) {
return; return;
} }
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) { if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
@@ -144,15 +144,16 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} }
prepareChartOptions(summary: AddressTxSummary[]) { prepareChartOptions(summary: AddressTxSummary[]) {
if (!summary || !this.stats) { if (!summary) {
return; return;
} }
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
let runningTotal = total;
const processData = summary.map(d => { const processData = summary.map(d => {
const balance = total; const balance = runningTotal;
const fiatBalance = total * d.price / 100_000_000; const fiatBalance = runningTotal * d.price / 100_000_000;
total -= d.value; runningTotal -= d.value;
return { return {
time: d.time * 1000, time: d.time * 1000,
balance, balance,
@@ -172,7 +173,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat); this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
} }
this.data.push( this.data.push(
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }} {value: [now, total], symbol: 'none', tooltip: { show: false }}
); );
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);

View File

@@ -1,15 +1,15 @@
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, catchError } from 'rxjs/operators'; import { switchMap, catchError } from 'rxjs/operators';
import { Address, Transaction } from '../../interfaces/electrs.interface'; import { Address, Transaction } from '@interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { of, Subscription, forkJoin } from 'rxjs'; import { of, Subscription, forkJoin } from 'rxjs';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { AddressInformation } from '../../interfaces/node-api.interface'; import { AddressInformation } from '@interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-address-group', selector: 'app-address-group',

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { Vin, Vout } from '../../interfaces/electrs.interface'; import { Vin, Vout } from '@interfaces/electrs.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AddressType, AddressTypeInfo } from '../../shared/address-utils'; import { AddressType, AddressTypeInfo } from '@app/shared/address-utils';
@Component({ @Component({
selector: 'app-address-labels', selector: 'app-address-labels',
@@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
} }
handleVin() { 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) { if (address?.scripts.size) {
const script = address?.scripts.values().next().value; const script = address?.scripts.values().next().value;
if (script.template?.label) { if (script.template?.label) {

View File

@@ -12,7 +12,7 @@
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate> <app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
</a> </a>
</td> </td>
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td> <td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" [digitsInfo]="getAmountDigits(transaction.value)" [noFiat]="true"></app-amount></td>
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td> <td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td> <td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
</tr> </tr>

View File

@@ -1,9 +1,9 @@
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface'; import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs'; import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
import { PriceService } from '../../services/price.service'; import { PriceService } from '@app/services/price.service';
@Component({ @Component({
selector: 'app-address-transactions-widget', selector: 'app-address-transactions-widget',
@@ -43,7 +43,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
startAddressSubscription(): void { startAddressSubscription(): void {
this.isLoading = true; this.isLoading = true;
if (!this.address || !this.addressInfo) { if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
return; return;
} }
this.transactions$ = (this.addressSummary$ || (this.isPubkey this.transactions$ = (this.addressSummary$ || (this.isPubkey
@@ -55,7 +55,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
}) })
)).pipe( )).pipe(
map(summary => { map(summary => {
return summary?.slice(0, 6); return summary?.filter(tx => Math.abs(tx.value) >= 1000000)?.slice(0, 6);
}), }),
switchMap(txs => { switchMap(txs => {
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe( return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
@@ -68,6 +68,12 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
)))); ))));
}) })
); );
}
getAmountDigits(value: number): string {
const decimals = Math.max(0, 4 - Math.ceil(Math.log10(Math.abs(value / 100_000_000))));
return `1.${decimals}-${decimals}`;
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@@ -1,16 +1,16 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, Transaction } from '../../interfaces/electrs.interface'; import { Address, Transaction } from '@interfaces/electrs.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { OpenGraphService } from '../../services/opengraph.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs'; import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils'; import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface'; import { AddressInformation } from '@interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-address-preview', selector: 'app-address-preview',

View File

@@ -94,6 +94,20 @@
</div> </div>
</ng-container> </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> <br>
<div class="title-tx"> <div class="title-tx">
<h2 class="text-left"> <h2 class="text-left">
@@ -103,7 +117,7 @@
</h2> </h2>
</div> </div>
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [address]="address.address" (loadMore)="loadMore()"></app-transactions-list> <app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="[address.address]" (loadMore)="loadMore()"></app-transactions-list>
<div class="text-center"> <div class="text-center">
<ng-template [ngIf]="isLoadingTransactions"> <ng-template [ngIf]="isLoadingTransactions">

View File

@@ -1,17 +1,17 @@
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; 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 { WebsocketService } from '@app/services/websocket.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/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 { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils'; import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface'; import { AddressInformation } from '@interfaces/node-api.interface';
import { AddressTypeInfo } from '../../shared/address-utils'; import { AddressTypeInfo } from '@app/shared/address-utils';
class AddressStats implements ChainStats { class AddressStats implements ChainStats {
address: string; address: string;
@@ -104,6 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
addressString: string; addressString: string;
isLoadingAddress = true; isLoadingAddress = true;
transactions: Transaction[]; transactions: Transaction[];
utxos: Utxo[];
isLoadingTransactions = true; isLoadingTransactions = true;
retryLoadMore = false; retryLoadMore = false;
error: any; error: any;
@@ -159,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.address = null; this.address = null;
this.isLoadingTransactions = true; this.isLoadingTransactions = true;
this.transactions = null; this.transactions = null;
this.utxos = null;
this.addressInfo = null; this.addressInfo = null;
this.exampleChannel = null; this.exampleChannel = null;
document.body.scrollTo(0, 0); document.body.scrollTo(0, 0);
@@ -212,11 +214,23 @@ export class AddressComponent implements OnInit, OnDestroy {
this.updateChainStats(); this.updateChainStats();
this.isLoadingAddress = false; this.isLoadingAddress = false;
this.isLoadingTransactions = true; 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.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; this.tempTransactions = transactions;
if (transactions.length) { if (transactions.length) {
this.lastTransactionTxId = transactions[transactions.length - 1].txid; this.lastTransactionTxId = transactions[transactions.length - 1].txid;
@@ -309,6 +323,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions = this.transactions.slice(); this.transactions = this.transactions.slice();
this.mempoolStats.removeTx(transaction); this.mempoolStats.removeTx(transaction);
this.audioService.playSound('magic'); this.audioService.playSound('magic');
this.confirmTransaction(tx);
} else { } else {
if (this.addTransaction(transaction, false)) { if (this.addTransaction(transaction, false)) {
this.audioService.playSound('magic'); 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; return true;
} }
@@ -346,9 +386,65 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions.splice(index, 1); this.transactions.splice(index, 1);
this.transactions = this.transactions.slice(); 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; 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 { loadMore(): void {
if (this.isLoadingTransactions || this.fullyLoaded) { if (this.isLoadingTransactions || this.fullyLoaded) {
return; return;

View File

@@ -0,0 +1,10 @@
<div class="addresses-treemap-container">
<div *ngIf="addresses" style="height: 300px">
<div *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
</div>
</div>
<div *ngIf="!stateService.isBrowser || isLoading" class="text-center loading-spinner">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
.node-channels-container {
position: relative;
}
.loading-spinner {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 100;
}
.spinner-border {
position: relative;
top: 225px;
}

View File

@@ -0,0 +1,150 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
import { Router } from '@angular/router';
import { EChartsOption, TreemapSeriesOption } from '@app/graphs/echarts';
import { lerpColor } from '@app/shared/graphs.utils';
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { LightningApiService } from '@app/lightning/lightning-api.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service';
import { Address } from '@interfaces/electrs.interface';
import { formatNumber } from '@angular/common';
@Component({
selector: 'app-addresses-treemap',
templateUrl: './addresses-treemap.component.html',
styleUrls: ['./addresses-treemap.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressesTreemap implements OnChanges {
@Input() addresses: Address[];
@Input() isLoading: boolean = false;
chartInstance: any;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
constructor(
@Inject(LOCALE_ID) public locale: string,
private lightningApiService: LightningApiService,
private amountShortenerPipe: AmountShortenerPipe,
private zone: NgZone,
private router: Router,
public stateService: StateService,
) {}
ngOnChanges(): void {
this.prepareChartOptions();
}
prepareChartOptions(): void {
const data = this.addresses.map(address => ({
address: address.address,
value: address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum,
stats: address.chain_stats,
}));
// only consider visible items for the color gradient
const totalValue = data.reduce((acc, address) => acc + address.value, 0);
const maxTxs = data.filter(address => address.value > (totalValue / 2000)).reduce((max, address) => Math.max(max, address.stats.tx_count), 0);
const dataItems = data.map(address => ({
...address,
itemStyle: {
color: lerpColor('#1E88E5', '#D81B60', address.stats.tx_count / maxTxs),
}
}));
this.chartOptions = {
tooltip: {
trigger: 'item',
textStyle: {
align: 'left',
}
},
series: <TreemapSeriesOption[]>[
{
height: 300,
left: 0,
right: 0,
bottom: 0,
top: 0,
roam: false,
type: 'treemap',
data: dataItems,
nodeClick: 'link',
progressive: 100,
tooltip: {
show: true,
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
},
borderColor: '#000',
formatter: (value): string => {
if (!value.data.address) {
return '';
}
return `
<table style="table-layout: fixed;">
<tbody>
<tr>
<td colspan="2"><b style="color: white; margin-left: 2px">${value.data.address}</b></td>
</tr>
<tr>
<td>Received</td>
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum)}</td>
</tr>
<tr>
<td>Sent</td>
<td style="text-align: right">${this.formatValue(value.data.stats.spent_txo_sum)}</td>
</tr>
<tr>
<td>Balance</td>
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum - value.data.stats.spent_txo_sum)}</td>
</tr>
<tr>
<td>Transaction count</td>
<td style="text-align: right">${value.data.stats.tx_count}</td>
</tr>
</tbody>
</table>
`;
}
},
itemStyle: {
borderColor: 'black',
borderWidth: 1,
},
breadcrumb: {
show: false,
}
}
]
};
}
formatValue(sats: number): string {
if (sats > 100000000) {
return formatNumber(sats / 100000000, this.locale, '1.2-2') + ' BTC';
} else {
return this.amountShortenerPipe.transform(sats, 2) + ' sats';
}
}
onChartInit(ec: any): void {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
//@ts-ignore
if (!e.data.address) {
return;
}
this.zone.run(() => {
//@ts-ignore
const url = new RelativeUrlPipe(this.stateService).transform(`/address/${e.data.address}`);
this.router.navigate([url]);
});
});
}
}

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 '@app/services/storage.service';
import { StateService } from '@app/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') { @if (digitsInfo === '1.8-8') {
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }} &lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
} @else { } @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"> <span class="symbol">
<ng-container *ngTemplateOutlet="prefix"></ng-container>sats <ng-container *ngTemplateOutlet="prefix"></ng-container>sats

View File

@@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { Price } from '../../services/price.service'; import { Price } from '@app/services/price.service';
@Component({ @Component({
selector: 'app-amount', selector: 'app-amount',

View File

@@ -1,11 +1,11 @@
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core'; import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router'; import { Router, NavigationEnd } from '@angular/router';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { OpenGraphService } from '../../services/opengraph.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap'; import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
import { ThemeService } from '../../services/theme.service'; import { ThemeService } from '@app/services/theme.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',

View File

@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { moveDec } from '../../bitcoin.utils'; import { moveDec } from '@app/bitcoin.utils';
import { AssetsService } from '../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { environment } from '../../../environments/environment'; import { environment } from '@environments/environment';
@Component({ @Component({
selector: 'app-asset-circulation', selector: 'app-asset-circulation',

View File

@@ -1,17 +1,17 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, filter, catchError, take } from 'rxjs/operators'; import { switchMap, filter, catchError, take } from 'rxjs/operators';
import { Asset, Transaction } from '../../interfaces/electrs.interface'; import { Asset, Transaction } from '@interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { of, merge, Subscription, combineLatest } from 'rxjs'; import { of, merge, Subscription, combineLatest } from 'rxjs';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { environment } from '../../../environments/environment'; import { environment } from '@app/../environments/environment';
import { AssetsService } from '../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
import { moveDec } from '../../bitcoin.utils'; import { moveDec } from '@app/bitcoin.utils';
@Component({ @Component({
selector: 'app-asset', selector: 'app-asset',

View File

@@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { AssetsService } from '../../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
@Component({ @Component({
selector: 'app-asset-group', selector: 'app-asset-group',

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from '../../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
@Component({ @Component({
selector: 'app-assets-featured', selector: 'app-assets-featured',

View File

@@ -4,12 +4,12 @@ import { Router } from '@angular/router';
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import { merge, Observable, of, Subject } from 'rxjs'; import { merge, Observable, of, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
import { AssetExtended } from '../../../interfaces/electrs.interface'; import { AssetExtended } from '@interfaces/electrs.interface';
import { AssetsService } from '../../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { environment } from '../../../../environments/environment'; import { environment } from '@environments/environment';
@Component({ @Component({
selector: 'app-assets-nav', selector: 'app-assets-nav',

View File

@@ -1,13 +1,13 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { AssetsService } from '../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
import { environment } from '../../../environments/environment'; import { environment } from '@environments/environment';
import { UntypedFormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { filter, map, switchMap, take } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { AssetExtended } from '../../interfaces/electrs.interface'; import { AssetExtended } from '@interfaces/electrs.interface';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
@Component({ @Component({
selector: 'app-assets', selector: 'app-assets',

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