Compare commits

...

327 Commits

Author SHA1 Message Date
Mononaut
f3fc774c2d Add standalone block visualization page 2023-09-21 21:57:54 +01:00
softsimon
d0696628b2 Merge pull request #4200 from mempool/mononaut/canonical-blocks
Update canonical link with block hash
2023-09-21 12:45:21 +04:00
Mononaut
368ab1dc66 Update canonical link with block hash 2023-09-21 12:31:38 +04:00
softsimon
b6f66a6a28 Merge pull request #4236 from mempool/mononaut/mempool-count-graph
Add mempool transaction count line to graph
2023-09-20 20:26:25 +04:00
softsimon
bda361cf57 Merge pull request #4269 from mempool/mononaut/reduce-blockchain-jumping
Reduce blockchain jumping
2023-09-20 18:20:34 +04:00
Mononaut
1a6a0c12ec Fix blockchain scroll jumping 2023-09-20 03:55:06 +00:00
Mononaut
b645ad3fd8 Reduce thrashing while initializing blockchain scroll position 2023-09-19 22:55:57 +00:00
wiz
b979594a04 Merge pull request #4262 from mempool/wiz/add-va1-mempoolspace-lightning-nodes
Add VA1 mempool.space lightning node pubkeys
2023-09-18 17:43:40 -04:00
wiz
6aceb898df Merge pull request #4248 from orangesurf/master
Add orangesurf contributor declaration & update trademark to registered trademark
2023-09-18 17:27:50 -04:00
Mononaut
c80201e3db hide mempool count line before start of data 2023-09-18 21:13:11 +00:00
Mononaut
ef5d2606b7 Add missing count data to longer statistics timespans 2023-09-18 21:07:24 +00:00
softsimon
1e648a2dce Merge pull request #4265 from mempool/nymkappa/fix-js-error-when-no-geolocation
[lightning] fix js error when there is no geolocation available
2023-09-18 17:23:23 +04:00
softsimon
11058b469c Merge pull request #4209 from mempool/mononaut/address-page-rbf
Notify address page of removed transactions
2023-09-18 17:04:31 +04:00
nymkappa
e79c32010d [lightning] fix js error when there is no geolocation available 2023-09-18 09:44:22 +02:00
wiz
5e4742e51b Merge branch 'master' into master 2023-09-18 00:51:38 -04:00
wiz
38d30d23e5 Add VA1 mempool.space lightning node pubkeys 2023-09-17 20:57:47 -04:00
wiz
47dd2cbd5d ops: Add VA1 servers to prod config 2023-09-17 19:56:33 -04:00
Mononaut
9b17c16325 Notify address page of removed transactions 2023-09-14 23:25:21 +00:00
softsimon
83c285e17d Merge pull request #4199 from mempool/mononaut/fix-diff-skeleton-mouseover
Fix js error on mouseover on difficulty skeleton
2023-09-13 13:26:10 +04:00
softsimon
5fabf892ba Merge branch 'master' into mononaut/fix-diff-skeleton-mouseover 2023-09-13 13:22:19 +04:00
orangesurf
0ba4e98f52 Replace other instances of tm use for The Mempool Open Source Project 2023-09-12 19:55:00 +01:00
orangesurf
a1b75c3772 Update to reflect registered trademark 2023-09-12 19:45:18 +01:00
orangesurf
0712195de4 Add orangesurf contributor declaration 2023-09-12 19:29:33 +01:00
Mononaut
71d4aa9ddd Add mempool count line to graph 2023-09-08 01:35:02 +09:00
wiz
827b0f6ad1 ops: Make install script wait longer for mysql to start 2023-08-31 16:52:56 +09:00
wiz
e8d85613b4 Merge pull request #4224 from mempool/mononaut/failover-timeout
Bump failover timeout to 5s
2023-08-31 02:38:10 +09:00
wiz
ce3b599bab ops: Add /api/v1/services route for new backend 2023-08-31 02:36:22 +09:00
Mononaut
47a7564cfc Bump failover timeout to 5s 2023-08-31 02:25:28 +09:00
wiz
7744146ef7 Merge pull request #4188 from mempool/nymkappa/menu
User menu + integrated accelerator if available
2023-08-31 01:55:52 +09:00
wiz
9cb72cff31 Set apple-mobile-web-app-capable to yes 2023-08-31 01:35:10 +09:00
nymkappa
98119b3e54 Merge remote-tracking branch 'origin/nymkappa/menu' into nymkappa/menu 2023-08-30 18:32:56 +02:00
nymkappa
f5a3e78cbe [css] fix spacing at the top x2 2023-08-30 18:32:14 +02:00
wiz
515d2656e7 Set apple-mobile-web-app-status-bar-style to black 2023-08-31 01:23:07 +09:00
wiz
339a21caaa Merge branch 'master' into nymkappa/menu 2023-08-31 01:01:00 +09:00
wiz
a7c0d33de8 Merge pull request #4219 from mempool/mononaut/accelerator-preview-concept
Accelerator preview concept
2023-08-31 00:58:24 +09:00
Mononaut
d1ef1afc9c Fix default min/max/default userbid 2023-08-31 00:32:54 +09:00
wiz
bf4a1fcd7a Merge pull request #4221 from hunicus/add-meta-descriptions
Add meta descriptions
2023-08-31 00:29:46 +09:00
Mononaut
d6044331e1 Hide balance unless insufficient for max cost 2023-08-31 00:15:09 +09:00
wiz
9fdb164b64 Merge branch 'master' into add-meta-descriptions 2023-08-31 00:09:57 +09:00
hunicus
d691bf2714 Add meta titles+descriptions to pages missing them 2023-08-30 23:59:51 +09:00
hunicus
cd366177ba Add meta descriptions for mempool and liquid 2023-08-30 21:10:59 +09:00
hunicus
6312884234 Add meta descriptions for bisq 2023-08-30 20:47:57 +09:00
hunicus
d1f26f0491 Add meta descriptions to docs page
Also move all meta tag setting to ngDoCheck()
because page titles were not updating when
user switched docs tabs.
2023-08-30 20:47:57 +09:00
Mononaut
ed12e30517 Hide fee diagram on mobile 2023-08-30 17:16:39 +09:00
Mononaut
cb363aca23 Simplify acceleration quote 2023-08-30 16:55:24 +09:00
Mononaut
c753a8e92a Accelerator fee diagram concept 2023-08-30 16:55:24 +09:00
nymkappa
c44276027c [auto scroll] fix documention anchor scrolling 2023-08-29 15:51:17 +02:00
nymkappa
91fbd0864b [css] fix footer position 2023-08-29 14:34:56 +02:00
nymkappa
c2c4047ffd [css] fix spacing at the top 2023-08-29 14:19:02 +02:00
nymkappa
9427ba96a2 Merge branch 'master' into nymkappa/menu 2023-08-29 10:53:09 +02:00
nymkappa
7c11f0a3da [tx] fix eta css 2023-08-29 10:50:13 +02:00
softsimon
6780ba82d0 Merge pull request #4213 from mempool/mononaut/live-cpfp-updates
Send cpfp/effective fee rate changes to subscribed websocket clients
2023-08-29 10:17:33 +02:00
softsimon
6d643604d0 Merge branch 'master' into mononaut/live-cpfp-updates 2023-08-29 10:12:55 +02:00
nymkappa
7ae4b451e4 [auth] remove auto logout when imageMd5 no in localstorage 2023-08-29 09:31:18 +02:00
nymkappa
bbd1f088d1 [footer] only show cta if official && ACCELERATOR 2023-08-28 16:20:30 +02:00
nymkappa
3060aecddb [status badge] reposition a bit higher 2023-08-28 15:57:06 +02:00
nymkappa
7a765cecd9 [footer] only show cta if official 2023-08-28 15:41:45 +02:00
nymkappa
03e9592c79 Merge pull request #4216 from mempool/mononaut/fix-menu-blockchain-offset
Fix bad blockchain offset after services -> dash
2023-08-28 10:56:18 +02:00
Mononaut
c7b89f31dd Fix bad blockchain offset after services -> dash 2023-08-28 17:42:32 +09:00
nymkappa
899b230760 Merge remote-tracking branch 'origin/nymkappa/menu' into nymkappa/menu 2023-08-28 10:24:52 +02:00
nymkappa
d787ef99c4 [typo] the Mempool Accelerator -> Mempool Accelerator 2023-08-28 10:24:44 +02:00
wiz
043ab008db Merge branch 'master' into nymkappa/menu 2023-08-28 16:43:03 +09:00
nymkappa
4abc4e96a8 [typo] satss -> sats 2023-08-28 09:34:50 +02:00
nymkappa
7fd8790750 [auth] fix blinking profile picture 2023-08-28 09:29:13 +02:00
nymkappa
8ba4a7b421 [ui] polish x2 2023-08-27 19:17:03 +02:00
nymkappa
78ea9cbd16 [ui] polish x1 2023-08-27 12:52:58 +02:00
softsimon
727937b8ae Merge pull request #4135 from mempool/hunicus/citadel-link
Change citadel link on about page
2023-08-27 11:42:52 +02:00
nymkappa
5f4add3e22 [tx] fix css when accel not available 2023-08-27 09:07:47 +02:00
wiz
c4f8afbaf7 Merge pull request #4195 from mempool/junderw/rusttoolchain 2023-08-27 13:19:15 +09:00
Mononaut
528877f43f Send cpfp/effective fee rate changes to subscribed ws clients 2023-08-27 00:30:55 +09:00
nymkappa
a3d61fa525 [accelerator] show payment preview when not logged in 2023-08-26 15:07:05 +02:00
nymkappa
c0308fbc0d Merge branch 'master' into nymkappa/menu 2023-08-26 14:01:07 +02:00
softsimon
24696d0408 Merge pull request #4183 from mempool/mononaut/attitude-adjustment
Don't overload core with mempool tx requests
2023-08-26 12:28:26 +02:00
nymkappa
b9838fda8d Merge branch 'master' into mononaut/attitude-adjustment 2023-08-26 11:48:21 +02:00
nymkappa
726bd51abb [debug] update versioning print 2023-08-26 11:27:45 +02:00
nymkappa
1fe08d1234 [accelerator] fix overflow in tx page integrated accel 2023-08-26 10:05:04 +02:00
nymkappa
1ca136fa75 Merge pull request #4212 from mempool/nymkappa/accelerate-preview
Nymkappa/accelerate preview
2023-08-26 09:53:29 +02:00
nymkappa
fcecbe4967 [tx] integrated accelerator 2023-08-26 09:52:55 +02:00
hunicus
d42a3f74ec Add description to index html and seo service 2023-08-26 14:18:55 +09:00
wiz
dc44f1b618 ops: Fix WebGL for unfurler 2023-08-26 04:40:12 +09:00
wiz
0fde6dd908 ops: Bump prod NodeJS version to v20.5.1 2023-08-25 23:34:50 +09:00
wiz
4eb494baec ops: Increase FreeBSD bitcoin node shutdown timeout to 600 2023-08-25 23:29:03 +09:00
wiz
c1b2f1f2c7 ops: Disable tor in prod install script 2023-08-25 23:24:51 +09:00
wiz
a6d2887847 Merge branch 'master' into nymkappa/menu 2023-08-25 23:13:32 +09:00
nymkappa
f9895a492c Merge branch 'nymkappa/menu' into nymkappa/accelerate-preview 2023-08-24 18:23:30 +02:00
nymkappa
ac56f70f6f Merge pull request #4176 from mempool/nymkappa/fix-eta
[tx] fix eta css with accelerate button
2023-08-24 18:21:27 +02:00
nymkappa
c89e283c8e [tx] start adding acceleration preview 2023-08-24 14:17:31 +02:00
nymkappa
3aa938a94b Merge pull request #4202 from mempool/mononaut/dynamic-width-chain
Dynamic width chain
2023-08-24 10:42:46 +02:00
Mononaut
20fff97804 restore window resize listener 2023-08-24 17:28:21 +09:00
Mononaut
d1f7026804 hambuger 2023-08-24 17:28:19 +09:00
Mononaut
2da31c4d4a Slide blockchain to compensate for menu 2023-08-24 17:27:31 +09:00
Mononaut
975ba653a2 Dynamically resize blockchain to fit container 2023-08-24 17:27:31 +09:00
softsimon
f12fabe030 Merge pull request #4206 from mempool/dependabot/npm_and_yarn/frontend/frontend-angular-dependencies-f1c021e5b3
Bump the frontend-angular-dependencies group in /frontend with 12 updates
2023-08-24 10:17:12 +02:00
dependabot[bot]
25d75efa09 Bump the frontend-angular-dependencies group
Bumps the frontend-angular-dependencies group in /frontend with 12 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular-devkit/build-angular](https://github.com/angular/angular-cli) | `16.1.4` | `16.2.0` |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `16.1.5` | `16.2.2` |
| [@angular/cli](https://github.com/angular/angular-cli) | `16.1.4` | `16.2.0` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `16.1.5` | `16.2.2` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `16.1.5` | `16.2.2` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `16.1.5` | `16.2.2` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `16.1.5` | `16.2.2` |
| [@angular/localize](https://github.com/angular/angular) | `16.1.5` | `16.2.2` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `16.1.5` | `16.2.2` |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `16.1.5` | `16.2.2` |
| [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) | `16.1.5` | `16.2.2` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `16.1.5` | `16.2.2` |


Updates `@angular-devkit/build-angular` from 16.1.4 to 16.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/16.1.4...16.2.0)

Updates `@angular/animations` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.2.2/packages/animations)

Updates `@angular/cli` from 16.1.4 to 16.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/16.1.4...16.2.0)

Updates `@angular/common` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.2.2/packages/common)

Updates `@angular/compiler` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.2.2/packages/compiler)

Updates `@angular/core` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.2.2/packages/core)

Updates `@angular/forms` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.2.2/packages/forms)

Updates `@angular/localize` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/compare/16.1.5...16.2.2)

Updates `@angular/platform-browser` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.2.2/packages/platform-browser)

Updates `@angular/platform-browser-dynamic` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.2.2/packages/platform-browser-dynamic)

Updates `@angular/platform-server` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.2.2/packages/platform-server)

Updates `@angular/router` from 16.1.5 to 16.2.2
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.2.2/packages/router)

---
updated-dependencies:
- dependency-name: "@angular-devkit/build-angular"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/animations"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/cli"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/common"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-server"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-24 02:53:59 +00:00
nymkappa
c8100712e8 [footer] polish breakpoints in /services/* 2023-08-23 16:17:07 +02:00
nymkappa
a7b2c39f51 [footer] re-add missing tagline 2023-08-23 15:02:36 +02:00
nymkappa
3d9fa5076b Merge remote-tracking branch 'origin/nymkappa/menu' into nymkappa/menu 2023-08-23 14:40:29 +02:00
nymkappa
9fa715e403 [footer] re-add main CTA with better design 2023-08-23 14:40:16 +02:00
nymkappa
9ee11b64e9 [menu] disable hamburger icon when not logged in 2023-08-23 14:39:58 +02:00
nymkappa
8c90d4ba98 Merge branch 'master' into nymkappa/menu 2023-08-23 12:21:23 +02:00
nymkappa
b333c211f7 [menu] fix non clickable hamburger when logged out 2023-08-23 11:50:47 +02:00
wiz
b180fe694f ops: Tweak nginx config for gone paths and render expires 2023-08-23 01:09:11 +09:00
wiz
6c7d33f681 Merge pull request #4203 from mempool/mononaut/fix-unfurl-fallbacks
Fix unfurl fallback img routes
2023-08-23 00:31:48 +09:00
Mononaut
6946bc9da9 Fix unfurl fallback img routes 2023-08-23 00:11:24 +09:00
nymkappa
8fb566858f [css] make sure <main> cannot be larger than viewport 2023-08-22 15:44:32 +02:00
nymkappa
379e1470fd [menu] fix menu close trigger, fix menu scroll on mobile safari 2023-08-22 14:59:23 +02:00
nymkappa
c5f0608b46 [debug] footer version same color, inline version in about page 2023-08-22 14:51:05 +02:00
wiz
063d7e96a1 Merge pull request #4198 from mempool/mononaut/unfurler-fixes
More unfurler fixes
2023-08-22 20:26:03 +09:00
nymkappa
7544357826 [ui] redesign UX for navbar using sticky 2023-08-22 08:35:10 +02:00
nymkappa
5005817529 [ui] tweak profile image 2023-08-21 23:18:55 +02:00
nymkappa
bfc3fbe397 [ui] hide the navbar on scroll 2023-08-21 22:19:05 +02:00
nymkappa
30be7e0f7a [ui] redesign UX for navbar 2023-08-21 22:08:25 +02:00
junderw
18eb6600b0 Fix README to include workaround for legacy versions of npm 2023-08-21 12:53:20 -07:00
junderw
88c0d04c18 Match CI toolchain with rust-toolchain 2023-08-21 12:53:20 -07:00
Mononaut
8ea7bb907c Fix js error on mouseover on difficulty skeleton 2023-08-22 02:40:00 +09:00
Mononaut
0d2df72621 Proxy unfurler resources to local instance 2023-08-22 02:16:46 +09:00
Mononaut
91c0a3e689 More unfurler logging 2023-08-22 02:16:36 +09:00
junderw
b595002c25 Revert "Add a missing [workspace] tag in rust-gbt Cargo to build in git"
This reverts commit fadc46f3b5.
2023-08-21 10:01:40 -07:00
Mononaut
9ba7ab9975 More robust webgl handling 2023-08-22 00:07:44 +09:00
softsimon
7674bee5ad Animating menu 2023-08-21 17:06:12 +02:00
nymkappa
0ee28a335f [header] show anon image if user did not set a picture (to improve later) 2023-08-21 17:04:17 +02:00
nymkappa
285485c69e [about page] /api/v1/about-page -> /api/v1/services/sponsors 2023-08-21 15:27:55 +02:00
nymkappa
b8222bff43 [auth] fix auth not being cleared after logout 2023-08-21 15:01:31 +02:00
nymkappa
3321ae0f1f [debug] always show git hashes in footer 2023-08-21 11:36:40 +02:00
nymkappa
4010047344 Merge remote-tracking branch 'origin/nymkappa/menu' into nymkappa/menu 2023-08-21 11:20:20 +02:00
nymkappa
ce290a449c [debug] show more git hashes in about and footer 2023-08-21 11:19:54 +02:00
Jonathan Underwood
b9c151f549 Update rust-toolchain 2023-08-21 17:47:58 +09:00
nymkappa
2ff3e4acb9 Merge branch 'master' into nymkappa/menu 2023-08-21 09:14:24 +02:00
wiz
ead32a4a65 Merge pull request #4194 from TheBlueMatt/msrv
Drop MSRV to 1.63
2023-08-21 16:06:13 +09:00
nymkappa
a1e2d2fd74 [debug] SERVICES -> GIT_COMMIT_HASH_MEMPOOL_SPACE in env 2023-08-21 08:57:27 +02:00
Matt Corallo
122a721d7a sign cla 2023-08-21 05:04:33 +00:00
Matt Corallo
6088fffc09 Drop MSRV to 1.63
Debian bookworm ships with 1.63, and since the change is trivial,
there's little reason to not support people running mempool on
Debian.
2023-08-21 04:03:37 +00:00
Matt Corallo
fadc46f3b5 Add a missing [workspace] tag in rust-gbt Cargo to build in git
If cargo detects its being run in a git tree, it looks for a
top-level `Cargo.toml`. When failing to find one, it errors out,
saying "current package believes it's in a workspace when it's not."

Instead, we add a `[workspace]` tag to let cargo know that rust-gbt
is not, in fact, in a rust workspace.
2023-08-21 04:00:52 +00:00
nymkappa
e82b43e340 [footer] fix footer sizing in /services 2023-08-20 22:54:12 +02:00
nymkappa
22886cb32d [debug] show services backend version in /services global footer, show services global frontend build in /about 2023-08-20 22:53:33 +02:00
nymkappa
ef554ad67b Merge branch 'master' into nymkappa/menu 2023-08-20 08:21:38 +02:00
nymkappa
b64b6fb3c7 [menu] fix json.parse on missing auth in localstorage 2023-08-20 08:11:55 +02:00
wiz
8e73e76312 ops: Tweak proxy_cache_valid time for unfurler/slurper cache 2023-08-20 02:36:34 +09:00
wiz
9d978ead6d ops: Set expires headers for unfurler/slurper responses 2023-08-20 00:01:22 +09:00
wiz
655eb31107 ops: Tweak nginx cache valid time for unfurler/slurper 2023-08-19 23:27:08 +09:00
wiz
15bd7bc068 Merge pull request #4187 from mempool/mononaut/unfurler-debugging
Unfurler debugging
2023-08-19 23:23:53 +09:00
wiz
65847547b9 ops: Tweak nginx cache for slurper 2023-08-19 23:23:28 +09:00
Mononaut
ed9d31686e Add cluster/tab to unfurler logs 2023-08-19 21:07:10 +09:00
Mononaut
7f2a459575 Fix SSR puppeteer page initialization 2023-08-19 21:05:28 +09:00
Mononaut
126e87a746 More more verbose unfurler logs 2023-08-19 19:35:52 +09:00
Mononaut
2819cea509 Reduce core mempool tx sync to 8 concurrent requests 2023-08-19 19:02:30 +09:00
junderw
2a8a403da7 Use p-limit to limit concurrent requests 2023-08-19 18:53:32 +09:00
Mononaut
3bda5537d7 More verbose unfurler logs 2023-08-19 18:50:57 +09:00
Mononaut
df8b6cd53c Fix unfurler resource proxying 2023-08-19 18:40:11 +09:00
Mononaut
e4fcadf39b More verbose comments on $getMempoolTransactionsExtended 2023-08-19 04:47:19 +09:00
nymkappa
c4f2f4ca66 [menu] handle logout without reload, show signin in sidebar when not logged in 2023-08-18 18:33:09 +02:00
nymkappa
0c1221dc07 [menu] only show on official mainnet with accelerator enabled 2023-08-18 18:12:45 +02:00
nymkappa
d1c9e8b56e [menu] show username at the top of the menu 2023-08-18 18:04:40 +02:00
Mononaut
1b2122cd35 Don't overload core with mempool tx requests 2023-08-19 01:02:27 +09:00
nymkappa
5aff2c74e6 [menu] show hamburger when logged out, fix menu scrolling on small screen 2023-08-18 17:56:07 +02:00
softsimon
f36ee36576 Merge pull request #4180 from mempool/junderw/fix-nbits-calculations
Fix: calcBitsDifference regtest fix
2023-08-18 17:27:25 +02:00
junderw
08a09bf371 Fix: calcBitsDifference regtest fix 2023-08-17 23:57:20 -07:00
nymkappa
1bdbb1b908 [menu] logout is a special item 2023-08-17 22:13:06 +02:00
wiz
f1f97320df ops: Fix install script crash for removed electrs-start-liquidtestnet 2023-08-18 03:51:09 +09:00
softsimon
52cc3022c6 Merge pull request #4146 from mempool/mononaut/fix-mempool-blocks-stacking
Fix stacked mempool blocks layout & width
2023-08-17 19:17:19 +02:00
nymkappa
91e3943a74 [menu] call services api to fetch user menu 2023-08-17 18:59:27 +02:00
nymkappa
23b871631a [menu] write menu component with hardcoded values 2023-08-17 18:51:39 +02:00
wiz
e975bacaa1 Merge branch 'master' into mononaut/fix-mempool-blocks-stacking 2023-08-17 23:21:00 +09:00
nymkappa
ab911d5c9e [tx] fix eta css with accelerate button 2023-08-17 14:28:33 +02:00
wiz
2c9e20dd87 ops: Always start xorg for unfurler even if no GPU detected 2023-08-17 18:38:17 +09:00
wiz
487d5de0ef Merge pull request #4152 from mempool/mononaut/unfurler-mode-headers
Select unfurl mode with route prefix
2023-08-17 16:48:26 +09:00
wiz
1653f003bc Merge branch 'master' into mononaut/unfurler-mode-headers 2023-08-17 16:39:30 +09:00
softsimon
21ea6f82fc Merge pull request #4134 from mempool/knorrium/dependabot_frontend_groups
Add frontend dependency groups to dependabot
2023-08-17 06:22:11 +02:00
softsimon
ca682d5879 Merge pull request #4164 from mempool/knorrium/fix_docker_start
Fix Docker start script and unit test
2023-08-17 06:20:39 +02:00
Felipe Knorr Kuhn
cd3b11407b Merge branch 'master' into knorrium/dependabot_frontend_groups 2023-08-16 21:15:28 -07:00
Felipe Knorr Kuhn
004208e9c8 Fix linting 2023-08-16 16:16:04 -07:00
Felipe Knorr Kuhn
44d2ea1263 Fix Docker config unit test to work with array values 2023-08-16 16:08:45 -07:00
Felipe Knorr Kuhn
27797b8f1d Add missing ESPLORA FALLBACK vars to the start script 2023-08-16 16:08:01 -07:00
Felipe Knorr Kuhn
9aedccdb32 Remove trailing comma on the Docker JSON 2023-08-16 16:07:27 -07:00
Mononaut
b1e1601ccf Switch unfurl proxy header to url prefix 2023-08-16 21:55:03 +09:00
wiz
681d4bffd7 Merge branch 'master' into mononaut/unfurler-mode-headers 2023-08-16 04:07:35 +09:00
wiz
8f2a2d1c81 ops: Add delay before warm cache loop begins 2023-08-16 04:03:17 +09:00
wiz
0bfd01e732 ops: Add unfurl/slurp queries to nginx warm cache script 2023-08-16 03:51:10 +09:00
wiz
83a92a7e3a ops: Add nginx config for /unfurler and /slurper prefixes 2023-08-16 03:27:09 +09:00
softsimon
dbdbe29281 Merge pull request #4153 from mempool/hunicus/rm-luminex
Remove luminex from about page
2023-08-14 21:30:44 +01:00
hunicus
a2f67990b5 Remove luminex from about page 2023-08-15 04:24:34 +09:00
Mononaut
5511795fbb Fix mempool slide on page change 2023-08-14 19:34:31 +09:00
Mononaut
13f6f9f9e5 Select unfurl mode with X-Unfurl-Type header 2023-08-14 18:05:09 +09:00
Mononaut
8c21d106fc Fix stacked mempool blocks layout & width 2023-08-14 02:37:46 +09:00
wiz
e9165e5dd8 Merge pull request #4107 from mempool/mononaut/unfurler-fixes
Quick fixes for unfurler/seo renderer
2023-08-13 15:40:22 +09:00
wiz
2e8a9c75ee Merge pull request #4138 from mempool/mononaut/accelerate-mainnet-only
Hide accelerate CTA on non-mainnet networks
2023-08-12 17:19:46 +09:00
Mononaut
e8e245cc75 Hide accelerate CTA on non-mainnet networks 2023-08-10 20:27:49 +09:00
hunicus
45ba50e82c Change citadel link on about page 2023-08-10 16:26:07 +09:00
wiz
b6da116dfc Merge branch 'master' into mononaut/unfurler-fixes 2023-08-10 16:02:48 +09:00
wiz
a9e92d0593 Merge pull request #4121 from mempool/hunicus/footer-refinement-exp
Refine footer design/layout
2023-08-10 16:02:20 +09:00
wiz
db90e77a32 Merge branch 'master' into hunicus/footer-refinement-exp 2023-08-10 15:51:07 +09:00
Felipe Knorr Kuhn
c2cad416e2 Add frontend dependency groups to dependabot 2023-08-09 22:33:59 -07:00
wiz
7191ca6915 Merge pull request #4133 from mempool/knorrium/dependabot_tweak 2023-08-10 13:59:19 +09:00
Felipe Knorr Kuhn
fb3cb127b0 Set dependabot to fix the package.json file in addition to the lock file 2023-08-09 21:51:11 -07:00
wiz
9f007d78f8 ops: Fix fonts config from installer 2023-08-09 23:44:03 +09:00
wiz
c5bdcec164 ops: Add delay before starting bitcoind/elementsd/electrs 2023-08-09 23:43:49 +09:00
hunicus
d4f9600ff1 Make footer refinements responsive 2023-08-09 16:42:16 +09:00
softsimon
8b260ce21c Merge pull request #4108 from mempool/simon/disable-historical-testnets
Handle historical price API calls when using any testnet
2023-08-09 10:51:58 +04:00
softsimon
8d7c04bcda Merge pull request #4111 from mempool/nymkappa/mining-pool-api
[mining] add /api/v1/pools API to list mining pools
2023-08-07 14:33:58 +09:00
nymkappa
e4c17e5011 [mining] add /api/v1/mining/pools API to list mining pools 2023-08-07 12:16:01 +09:00
softsimon
b03c3745a2 Merge pull request #4110 from mempool/knorrium/backend_unit_test_tweaks
Backend unit test tweaks
2023-08-07 12:14:01 +09:00
softsimon
41eecfa7df Merge pull request #4095 from mempool/mononaut/refactor-address-tracking
Refactor websocket address tracking
2023-08-07 12:13:07 +09:00
Mononaut
38e9021e8c simplify scriptpubkey tracking 2023-08-07 10:43:42 +09:00
Felipe Knorr Kuhn
adc46b6ae5 Update Github workflow to run with the CI flag 2023-08-06 08:01:28 -07:00
Felipe Knorr Kuhn
8912bac0ac Add test:ci task 2023-08-06 08:00:49 -07:00
Felipe Knorr Kuhn
e4fca3c2b7 Log verbose Docker checks only when running on CI 2023-08-06 08:00:30 -07:00
hunicus
2855fff702 Make basic footer refinement
TODO: responsiveness
2023-08-06 16:16:16 +09:00
Mononaut
8dbf9f29cf Send unfurler fallback imgs directly 2023-08-06 15:49:11 +09:00
softsimon
cbebbd40f1 Handle price API in the frontend when testnet 2023-08-06 15:41:57 +09:00
Mononaut
9aa778e44e reboot unfurler periodically 2023-08-06 13:47:38 +09:00
Mononaut
6fc645a454 Remove unfurler soft404 debug spam 2023-08-06 13:47:20 +09:00
softsimon
b977c4332f Merge pull request #4076 from mempool/knorrium/update_cypress_deps
Update Cypress deps
2023-08-06 13:06:46 +09:00
Felipe Knorr Kuhn
a79e214a6a Merge branch 'master' into knorrium/update_cypress_deps 2023-08-05 14:24:24 -07:00
wiz
6f11defea2 Merge pull request #4103 from mempool/ops/enable-prod-replication
Enable audit replication for production
2023-08-05 22:10:10 +09:00
wiz
53ba48de9f ops: Enable replication for production 2023-08-05 21:57:43 +09:00
wiz
3fb097bfff Merge pull request #4099 from mempool/mononaut/esplora-failover
new health-check based esplora failover mechanism
2023-08-05 20:59:30 +09:00
Mononaut
9138c3b676 always switch back to local if available 2023-08-05 20:39:02 +09:00
softsimon
2ceafcacc6 Disable historical prices on testnets 2023-08-05 20:29:00 +09:00
Mononaut
85935d8f90 allow protocol, port & path in fallback urls 2023-08-05 20:06:19 +09:00
Mononaut
ae5e1e6d29 Fix failover debug prints 2023-08-05 19:55:33 +09:00
softsimon
bcfc704f7a Merge pull request #4102 from mempool/hunicus/footer-space
Add space below logo in footer
2023-08-05 19:37:41 +09:00
Mononaut
e512feef74 Add debug prints, fix POST request 2023-08-05 19:25:00 +09:00
softsimon
b11e31e54b Merge branch 'master' into knorrium/update_cypress_deps 2023-08-05 19:13:19 +09:00
hunicus
b1ed05e95e Add space below logo in footer 2023-08-05 19:06:25 +09:00
Mononaut
2095f90262 new health-check based esplora failover mechanism 2023-08-05 19:02:46 +09:00
wiz
794a4ded9c Merge pull request #3169 from mempool/mononaut/seo-ssr
dynamically render search crawler requests
2023-08-05 18:34:45 +09:00
wiz
cf4f779e59 Merge remote-tracking branch 'origin/master' into mononaut/seo-ssr 2023-08-05 18:23:12 +09:00
softsimon
c1c69d7272 Merge pull request #4096 from mempool/mononaut/speed-up-rbf-detection
Speed up RBF detection
2023-08-05 17:37:50 +09:00
softsimon
a4e7219214 Adding RBF tests 2023-08-05 17:20:07 +09:00
softsimon
af8e5b60ee Merge pull request #4098 from mempool/fix/liquid-bits-calc
Fix: Difficulty calculations for Liquid networks must be NaN
2023-08-05 17:10:08 +09:00
Jonathan Underwood
6ff7a59bfb Merge branch 'master' into fix/liquid-bits-calc 2023-08-05 00:00:02 -07:00
softsimon
95341806c3 Merge pull request #4100 from mempool/feat/rust-build-revert
Feature: Build Rust during build script AND install
2023-08-05 15:58:16 +09:00
junderw
e994aac162 Feature: Build Rust during build script AND install 2023-08-04 23:40:10 -07:00
junderw
ea926660fe Fix: Difficulty calculations for Liquid networks must be NaN 2023-08-04 20:42:14 -07:00
Felipe Knorr Kuhn
33a6f81265 Merge branch 'master' into knorrium/update_cypress_deps 2023-08-04 08:51:00 -07:00
Felipe Knorr Kuhn
367bcbda83 Update cypress action to node 18 2023-08-04 08:40:15 -07:00
Felipe Knorr Kuhn
e3114144e1 Tweak imports again 2023-08-04 08:36:05 -07:00
Felipe Knorr Kuhn
da4f891e2f Use full path for import 2023-08-04 08:23:29 -07:00
Felipe Knorr Kuhn
e4a43fcca5 Update tsconfig for the tests 2023-08-04 08:19:19 -07:00
Mononaut
3ec676ca90 Speed up RBF detection 2023-08-04 19:11:49 +09:00
softsimon
9da9c2750d Merge pull request #4064 from mempool/mononaut/better-mobile-difficulty-tooltip
Improve difficulty tooltip display on mobile
2023-08-04 18:41:07 +09:00
softsimon
d2641cc927 Merge pull request #4085 from mempool/mononaut/fast-mempool-sync
use bulk mempool post api to batch mempool update requests
2023-08-04 17:26:01 +09:00
softsimon
b026f5a481 Removing console log 2023-08-04 15:55:22 +09:00
softsimon
45ddab519d Merge pull request #4093 from mempool/fix/packaging-rust-gbt
Fix: Rust-GBT packaging needs fixing
2023-08-04 15:52:20 +09:00
junderw
0535e8c5f9 Fix: Rust-GBT packaging needs fixing 2023-08-04 15:45:34 +09:00
softsimon
b4ec69ce7a Merge pull request #4058 from mempool/mononaut/prices-api
Current fiat prices API
2023-08-04 15:39:35 +09:00
wiz
989e4832cc Merge pull request #3995 from mempool/mononaut/acceleration-viz
Acceleration visualization
2023-08-04 15:34:02 +09:00
softsimon
0d777c24c5 Merge pull request #4090 from mempool/mononaut/missing-latest-replacements
resume tracking subscriptions after websocket reconnect
2023-08-04 15:33:33 +09:00
mononaut
101de3bac7 Fix PRICE_UPDATES_PER_HOUR docker default
Co-authored-by: nymkappa <9780671+nymkappa@users.noreply.github.com>
2023-08-04 15:23:55 +09:00
wiz
c3f83f74ce Merge branch 'master' into mononaut/acceleration-viz 2023-08-04 15:10:02 +09:00
wiz
39e36936bc Merge pull request #3795 from mempool/nymkappa/accelerator-soft-launch
[accelerator] prepare soft launch
2023-08-04 14:31:39 +09:00
nymkappa
11248821c5 hide sponsor buttons 2023-08-04 14:30:40 +09:00
Mononaut
3074d814e7 precompute address transactions for websocket msg loop 2023-08-04 13:59:49 +09:00
softsimon
22aa45f055 Merge pull request #4094 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.6.0
Bump mysql2 from 3.5.2 to 3.6.0 in /backend
2023-08-04 13:56:27 +09:00
softsimon
07f95acc29 Base expiry on update frequency 2023-08-04 13:26:19 +09:00
softsimon
2892bfa1d8 Fixing cycle reset at top of the hour 2023-08-04 13:23:09 +09:00
dependabot[bot]
356ab9c6ae Bump mysql2 from 3.5.2 to 3.6.0 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.5.2 to 3.6.0.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.5.2...v3.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-04 02:40:58 +00:00
nymkappa
851d030878 [about] re-enable two sponsor buttons 2023-08-04 10:23:15 +09:00
softsimon
813f3dc09d Updating config sample 2023-08-03 18:26:24 +09:00
softsimon
172c77328b Changing setting to per hour 2023-08-03 18:22:52 +09:00
softsimon
b213f43a91 Erasing unused PRICE_DATA_SERVER from readme 2023-08-03 17:43:03 +09:00
softsimon
284d39baa8 Update price at least every hour 2023-08-03 17:43:02 +09:00
softsimon
af4d0b4d3f Allow priceUpdater to run without storing to database 2023-08-03 17:43:02 +09:00
softsimon
83a487ecae Setting fixture to fixed number 2023-08-03 17:43:02 +09:00
softsimon
bcb3b39bd8 Updating start.sh and README 2023-08-03 17:43:02 +09:00
softsimon
ae59f95ba9 Refactoring price update config. Fixing last price time. 2023-08-03 17:43:02 +09:00
softsimon
b2d4000b2d Update backend/src/api/prices/prices.routes.ts 2023-08-03 17:43:02 +09:00
softsimon
864d9239ce Update backend/src/__fixtures__/mempool-config.template.json 2023-08-03 17:43:02 +09:00
Mononaut
09c23b1241 configurable price update frequency 2023-08-03 17:43:02 +09:00
Mononaut
22665f149b add /prices api endpoint 2023-08-03 17:43:01 +09:00
Mononaut
cb5b96485c resume tracking subscriptions after websocket reconnect 2023-08-03 16:17:17 +09:00
nymkappa
4ee703325a [footer] polish cta using update footer with logo 2023-08-03 16:12:59 +09:00
Mononaut
3d6a8a501d limit mouse events to difficulty bar 2023-08-03 15:51:09 +09:00
nymkappa
0111d8806b Merge branch 'mononaut/acceleration-viz' into nymkappa/accelerator-soft-launch 2023-08-03 15:43:28 +09:00
Mononaut
d06fe83bd9 Improve difficulty tooltip display on mobile 2023-08-03 15:36:36 +09:00
nymkappa
8936273aeb Merge branch 'master' into mononaut/acceleration-viz 2023-08-03 15:28:56 +09:00
nymkappa
99a60ab22a add ACCELERATOR frontend setting 2023-08-03 15:20:48 +09:00
nymkappa
7ec5d8265f toggle header visibility in master page component 2023-08-03 15:20:48 +09:00
nymkappa
1458e89f3a [footer] main cta points to /accelerator 2023-08-03 15:20:48 +09:00
nymkappa
5846862d55 [footer] dynamic CTA button based on login status 2023-08-03 15:20:48 +09:00
nymkappa
727d170c9c [tx] add accel shortcut into transaction component 2023-08-03 15:20:47 +09:00
nymkappa
6c2c62ba2e [lightning] claim your node button 2023-08-03 15:20:47 +09:00
nymkappa
f2ae858097 [sponsors] show profile by tiers on about page 2023-08-03 15:20:19 +09:00
nymkappa
d236d89717 [mining] send pool unique id in /pools API 2023-08-03 15:08:14 +09:00
softsimon
cac2a984ab Merge pull request #4089 from andrewtoth/andrewtoth/sig
Accept CLA
2023-08-03 12:59:18 +09:00
andrewtoth
e0b2ffa527 Accept CLA 2023-08-03 03:46:15 +00:00
softsimon
2c7919ace6 Merge pull request #4065 from mempool/mononaut/log-scale-fee-graph
Use log10 scale for projected block fee graph
2023-08-02 18:35:46 +09:00
wiz
e9cd41722b Merge pull request #4081 from mempool/mononaut/fix-null-coinbase-fee
fix null coinbase fees in block summary
2023-08-02 16:05:09 +09:00
Mononaut
ca0c6b5e6e use bulk mempool/txs post api to batch mempool update requests 2023-08-02 15:05:27 +09:00
softsimon
b1d5ba890f Merge pull request #4084 from mempool/hunicus/30-footer-yacht
Show tool list when no other networks available in footer
2023-08-02 14:44:19 +09:00
softsimon
9373fb3dd1 Minor ngIf else template optimization 2023-08-02 14:34:59 +09:00
softsimon
f8ca53fdf0 Merge pull request #4078 from mempool/fix/difficulty-calc
Fix: Use bits to calculate difficulty instead of floating points
2023-08-02 13:08:55 +09:00
hunicus
ddd5baf44e Show tool list when no other networks available 2023-08-02 12:46:59 +09:00
mononaut
b365ad3ba4 Merge branch 'master' into fix/difficulty-calc 2023-08-02 11:00:32 +09:00
softsimon
63993b01aa Merge pull request #4083 from mempool/mononaut/fix-partial-index-difficulty
fix partially indexed difficulty bug
2023-08-01 18:49:45 +09:00
Mononaut
ab784cede2 fix partially indexed difficulty bug 2023-08-01 18:41:48 +09:00
wiz
648a732352 Merge pull request #3853 from mempool/hunicus/accelerate-disclaimer
Replace disclaimer text regarding tx acceleration
2023-08-01 18:36:41 +09:00
wiz
e5b8b73077 Merge pull request #4080 from mempool/hunicus/30-footer
Add graphic to footer + simplify left footer column
2023-08-01 18:34:17 +09:00
Mononaut
543357f1db fix null coinbase fees in block summary 2023-08-01 17:33:03 +09:00
junderw
9bf334a22d Fix: Use bits to calculate difficulty instead of floating points 2023-08-01 00:52:45 -07:00
Felipe Knorr Kuhn
a23cd5ad29 Update Cypress deps 2023-07-31 14:44:59 -07:00
Mononaut
945a8ce92e Use log10 scale for projected block fee graph 2023-07-30 18:56:57 +09:00
nymkappa
f8faccd502 Merge branch 'master' into mononaut/acceleration-viz 2023-07-29 13:51:49 +09:00
nymkappa
67cff804a6 /accelerations -> /accelerator/accelerations 2023-07-26 15:08:35 +09:00
Mononaut
cde4af5930 fix mismatched use of gbt implementations 2023-07-26 15:08:35 +09:00
Mononaut
928a8be846 fix pool-dependent accelerated audit handling 2023-07-26 15:08:34 +09:00
Mononaut
7c641544b2 check in missing rust-gbt file 2023-07-26 15:08:34 +09:00
Mononaut
2a2aee21fb fix audit highlightning and fee ranges 2023-07-26 15:08:34 +09:00
Mononaut
3838d947b1 fix tests 2023-07-26 15:08:34 +09:00
Mononaut
ffc2b6c53c Add acceleration support to rust gbt 2023-07-26 15:08:34 +09:00
Mononaut
6494f890fe include per-tx pools in /accelerations endpoint 2023-07-26 15:08:34 +09:00
Mononaut
ba54bc9d15 support for acceleration mempool blocks animation 2023-07-26 15:08:33 +09:00
Mononaut
083bfdba06 Refactor accelerated audits 2023-07-26 15:08:33 +09:00
Mononaut
20b3ceab1e Implement accelerations API & config setting 2023-07-26 15:08:33 +09:00
Mononaut
c246db1cf9 Refactor acceleration tracking 2023-07-26 15:08:33 +09:00
Mononaut
aa24f6a84d use accelerated rates for block templates & show in viz 2023-07-26 15:08:33 +09:00
Mononaut
e489f713eb include accelerated tx data in block audits 2023-07-26 15:08:26 +09:00
hunicus
ce950d63cb Replace disclaimer text regarding tx acceleration 2023-06-12 07:07:15 -04:00
Mononaut
105cccf9b0 convert soft 404s to hard 404s in unfurler ssr 2023-03-09 02:34:21 -06:00
Mononaut
2f3e498906 fix canonical/meta tags in unfurler 2023-03-09 00:26:28 -06:00
Mononaut
82a808529b clean up unfurler meta html template 2023-03-08 01:47:35 -06:00
Mononaut
477f3bd70a add applebot & bingbot to seo user-agent detection 2023-03-08 01:11:54 -06:00
wiz
a874cdfb56 Merge branch 'master' into mononaut/seo-ssr 2023-03-08 15:29:18 +09:00
Felipe Knorr Kuhn
c3c44713ef Merge branch 'master' into mononaut/seo-ssr 2023-03-02 23:53:40 -08:00
mononaut
f6cae729a7 revert capitalization in title tag
Co-authored-by: wiz <j@wiz.biz>
2023-02-28 18:40:59 -06:00
Mononaut
b1e32ed55f Fix googlebot user-agent detection 2023-02-27 10:48:13 -06:00
Mononaut
2f27d9279d extend unfurler to dynamically render search crawler requests 2023-02-26 10:56:32 -06:00
235 changed files with 8180 additions and 2194 deletions

View File

@@ -1,6 +1,7 @@
version: 2
updates:
- package-ecosystem: npm
versioning-strategy: increase
directory: "/backend"
schedule:
interval: daily
@@ -14,6 +15,21 @@ updates:
- package-ecosystem: npm
directory: "/frontend"
versioning-strategy: increase
groups:
frontend-angular-dependencies:
patterns:
- "@angular*"
- "@ng-*"
- "ngx-*"
frontend-jest-dependencies:
patterns:
- "@types/jest"
- "jest"
frontend-eslint-dependencies:
patterns:
- "@typescript-eslint*"
- "eslint"
schedule:
interval: daily
open-pull-requests-limit: 10

View File

@@ -27,8 +27,17 @@ jobs:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
- name: Install 1.70.x Rust toolchain
uses: dtolnay/rust-toolchain@1.70
- name: Read rust-toolchain file from repository
id: gettoolchain
run: echo "::set-output name=toolchain::$(cat rust-toolchain)"
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
# Latest version available on this commit is 1.71.1
# Commit date is Aug 3, 2023
uses: dtolnay/rust-toolchain@f361669954a8ecfc00a3443f35f9ac8e610ffc06
with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
- name: Install
if: ${{ matrix.flavor == 'dev'}}
@@ -47,7 +56,7 @@ jobs:
- name: Unit Tests
if: ${{ matrix.flavor == 'dev'}}
run: npm run test
run: npm run test:ci
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
- name: Build

View File

@@ -38,7 +38,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16.15.0
node-version: 18
cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json

3
backend/.gitignore vendored
View File

@@ -45,3 +45,6 @@ testem.log
#System Files
.DS_Store
Thumbs.db
# package folder (npm run package output)
/package

View File

@@ -85,7 +85,7 @@ Install dependencies with `npm` and build the backend:
```
cd backend
npm install
npm install --no-install-links # npm@9.4.2 and later can omit the --no-install-links
npm run build
```

View File

@@ -32,7 +32,8 @@
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1
},
"CORE_RPC": {
"HOST": "127.0.0.1",
@@ -49,7 +50,8 @@
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"RETRY_UNIX_SOCKET_AFTER": 30000
"RETRY_UNIX_SOCKET_AFTER": 30000,
"FALLBACK": []
},
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",
@@ -115,10 +117,6 @@
"USERNAME": "",
"PASSWORD": ""
},
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "https://mempool.space/api/v1",
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
@@ -137,5 +135,9 @@
"trusted",
"servers"
]
},
"MEMPOOL_SERVICES": {
"API": "https://mempool.space/api",
"ACCELERATIONS": false
}
}

17
backend/npm_package.sh Executable file
View File

@@ -0,0 +1,17 @@
#/bin/sh
set -e
# Remove previous dist folder
rm -rf dist
# Build new dist folder
npm run build
# Remove previous package folder
rm -rf package
# Move JS and deps
mv dist package
cp -R node_modules package
# Remove symlink for rust-gbt and insert real folder
rm package/node_modules/rust-gbt
cp -R rust-gbt package/node_modules
# Clean up deps
npm run package-rm-build-deps

View File

@@ -0,0 +1,12 @@
#/bin/sh
set -e
# Cleaning up inside the node_modules folder
cd package/node_modules
rm -r \
typescript \
@typescript-eslint \
@napi-rs \
./rust-gbt/src \
./rust-gbt/Cargo.toml \
./rust-gbt/build.rs

View File

@@ -17,9 +17,9 @@
"crypto-js": "~4.1.1",
"express": "~4.18.2",
"maxmind": "~4.3.11",
"mysql2": "~3.5.2",
"rust-gbt": "file:./rust-gbt",
"mysql2": "~3.6.0",
"redis": "^4.6.6",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.13.0"
@@ -6102,9 +6102,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz",
"integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz",
"integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -12212,9 +12212,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz",
"integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz",
"integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==",
"requires": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",

View File

@@ -22,19 +22,20 @@
"main": "index.ts",
"scripts": {
"tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json",
"build": "npm run build-rust && npm run tsc && npm run create-resources",
"build": "npm run rust-build && npm run tsc && npm run create-resources",
"create-resources": "cp ./src/tasks/price-feeds/mtgox-weekly.json ./dist/tasks && node dist/api/fetch-version.js",
"package": "npm run build && rm -rf package && mv dist package && mv node_modules package && mv rust-gbt package && npm run package-rm-build-deps",
"package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint @napi-rs ../rust-gbt/target ../rust-gbt/node_modules ../rust-gbt/src)",
"package": "./npm_package.sh",
"package-rm-build-deps": "./npm_package_rm_build_deps.sh",
"start": "node --max-old-space-size=2048 dist/index.js",
"start-production": "node --max-old-space-size=16384 dist/index.js",
"reindex-updated-pools": "npm run start-production --update-pools",
"reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks",
"test": "./node_modules/.bin/jest --coverage",
"test:ci": "CI=true ./node_modules/.bin/jest --coverage",
"lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"",
"build-rust": "cd rust-gbt && npm install"
"rust-build": "cd rust-gbt && npm run build-release"
},
"dependencies": {
"@babel/core": "^7.21.3",
@@ -45,7 +46,7 @@
"crypto-js": "~4.1.1",
"express": "~4.18.2",
"maxmind": "~4.3.11",
"mysql2": "~3.5.2",
"mysql2": "~3.6.0",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",

View File

@@ -12,6 +12,10 @@ export interface ThreadTransaction {
effectiveFeePerVsize: number
inputs: Array<number>
}
export interface ThreadAcceleration {
uid: number
delta: number
}
export class GbtGenerator {
constructor()
/**
@@ -19,13 +23,13 @@ export class GbtGenerator {
*
* Rejects if the thread panics or if the Mutex is poisoned.
*/
make(mempool: Array<ThreadTransaction>, maxUid: number): Promise<GbtResult>
make(mempool: Array<ThreadTransaction>, accelerations: Array<ThreadAcceleration>, maxUid: number): Promise<GbtResult>
/**
* # Errors
*
* Rejects if the thread panics or if the Mutex is poisoned.
*/
update(newTxs: Array<ThreadTransaction>, removeTxs: Array<number>, maxUid: number): Promise<GbtResult>
update(newTxs: Array<ThreadTransaction>, removeTxs: Array<number>, accelerations: Array<ThreadAcceleration>, maxUid: number): Promise<GbtResult>
}
/**
* The result from calling the gbt function.

View File

@@ -1,6 +1,6 @@
use crate::{
u32_hasher_types::{u32hashset_new, U32HasherState},
ThreadTransaction,
ThreadTransaction, thread_acceleration::ThreadAcceleration,
};
use std::{
cmp::Ordering,
@@ -88,44 +88,49 @@ impl Ord for AuditTransaction {
}
#[inline]
fn calc_fee_rate(fee: f64, vsize: f64) -> f64 {
fee / (if vsize == 0.0 { 1.0 } else { vsize })
fn calc_fee_rate(fee: u64, vsize: f64) -> f64 {
(fee as f64) / (if vsize == 0.0 { 1.0 } else { vsize })
}
impl AuditTransaction {
pub fn from_thread_transaction(tx: &ThreadTransaction) -> Self {
pub fn from_thread_transaction(tx: &ThreadTransaction, maybe_acceleration: Option<Option<&ThreadAcceleration>>) -> Self {
let fee_delta = match maybe_acceleration {
Some(Some(acceleration)) => acceleration.delta,
_ => 0.0
};
let fee = (tx.fee as u64) + (fee_delta as u64);
// rounded up to the nearest integer
let is_adjusted = tx.weight < (tx.sigops * 20);
let sigop_adjusted_vsize = ((tx.weight + 3) / 4).max(tx.sigops * 5);
let sigop_adjusted_weight = tx.weight.max(tx.sigops * 20);
let effective_fee_per_vsize = if is_adjusted {
calc_fee_rate(tx.fee, f64::from(sigop_adjusted_weight) / 4.0)
let effective_fee_per_vsize = if is_adjusted || fee_delta > 0.0 {
calc_fee_rate(fee, f64::from(sigop_adjusted_weight) / 4.0)
} else {
tx.effective_fee_per_vsize
};
Self {
uid: tx.uid,
order: tx.order,
fee: tx.fee as u64,
fee,
weight: tx.weight,
sigop_adjusted_weight,
sigop_adjusted_vsize,
sigops: tx.sigops,
adjusted_fee_per_vsize: calc_fee_rate(tx.fee, f64::from(sigop_adjusted_vsize)),
adjusted_fee_per_vsize: calc_fee_rate(fee, f64::from(sigop_adjusted_vsize)),
effective_fee_per_vsize,
dependency_rate: f64::INFINITY,
inputs: tx.inputs.clone(),
relatives_set_flag: false,
ancestors: u32hashset_new(),
children: u32hashset_new(),
ancestor_fee: tx.fee as u64,
ancestor_fee: fee,
ancestor_sigop_adjusted_weight: sigop_adjusted_weight,
ancestor_sigop_adjusted_vsize: sigop_adjusted_vsize,
ancestor_sigops: tx.sigops,
score: 0.0,
used: false,
modified: false,
dirty: effective_fee_per_vsize != tx.effective_fee_per_vsize,
dirty: effective_fee_per_vsize != tx.effective_fee_per_vsize || fee_delta > 0.0,
}
}
@@ -156,7 +161,7 @@ impl AuditTransaction {
// grows, so if we think of 0 as "grew infinitely" then dependency_rate would be
// the smaller of the two. If either side is NaN, the other side is returned.
self.dependency_rate.min(calc_fee_rate(
self.ancestor_fee as f64,
self.ancestor_fee,
f64::from(self.ancestor_sigop_adjusted_weight) / 4.0,
))
}
@@ -172,7 +177,7 @@ impl AuditTransaction {
#[inline]
fn calc_new_score(&mut self) {
self.score = self.adjusted_fee_per_vsize.min(calc_fee_rate(
self.ancestor_fee as f64,
self.ancestor_fee,
f64::from(self.ancestor_sigop_adjusted_vsize),
));
}

View File

@@ -5,7 +5,7 @@ use tracing::{info, trace};
use crate::{
audit_transaction::{partial_cmp_uid_score, AuditTransaction},
u32_hasher_types::{u32hashset_new, u32priority_queue_with_capacity, U32HasherState},
GbtResult, ThreadTransactionsMap,
GbtResult, ThreadTransactionsMap, thread_acceleration::ThreadAcceleration,
};
const MAX_BLOCK_WEIGHT_UNITS: u32 = 4_000_000 - 4_000;
@@ -53,7 +53,13 @@ impl Ord for TxPriority {
// TODO: Make gbt smaller to fix these lints.
#[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)]
pub fn gbt(mempool: &mut ThreadTransactionsMap, max_uid: usize) -> GbtResult {
pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAcceleration], max_uid: usize) -> GbtResult {
let mut indexed_accelerations = Vec::with_capacity(max_uid + 1);
indexed_accelerations.resize(max_uid + 1, None);
for acceleration in accelerations {
indexed_accelerations[acceleration.uid as usize] = Some(acceleration);
}
let mempool_len = mempool.len();
let mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1);
audit_pool.resize(max_uid + 1, None);
@@ -63,7 +69,8 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, max_uid: usize) -> GbtResult {
info!("Initializing working structs");
for (uid, tx) in &mut *mempool {
let audit_tx = AuditTransaction::from_thread_transaction(tx);
let acceleration = indexed_accelerations.get(*uid as usize);
let audit_tx = AuditTransaction::from_thread_transaction(tx, acceleration.copied());
// Safety: audit_pool and mempool_stack must always contain the same transactions
audit_pool[*uid as usize] = Some(ManuallyDrop::new(audit_tx));
mempool_stack.push(*uid);
@@ -328,13 +335,15 @@ fn set_relatives(txid: u32, audit_pool: &mut AuditPool) {
let mut total_sigops: u32 = 0;
for ancestor_id in &ancestors {
let Some(ancestor) = audit_pool
if let Some(ancestor) = audit_pool
.get(*ancestor_id as usize)
.expect("audit_pool contains all ancestors") else { todo!() };
total_fee += ancestor.fee;
total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight;
total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize;
total_sigops += ancestor.sigops;
.expect("audit_pool contains all ancestors")
{
total_fee += ancestor.fee;
total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight;
total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize;
total_sigops += ancestor.sigops;
} else { todo!() };
}
if let Some(Some(tx)) = audit_pool.get_mut(txid as usize) {

View File

@@ -9,6 +9,7 @@
use napi::bindgen_prelude::Result;
use napi_derive::napi;
use thread_transaction::ThreadTransaction;
use thread_acceleration::ThreadAcceleration;
use tracing::{debug, info, trace};
use tracing_log::LogTracer;
use tracing_subscriber::{EnvFilter, FmtSubscriber};
@@ -19,6 +20,7 @@ use std::sync::{Arc, Mutex};
mod audit_transaction;
mod gbt;
mod thread_transaction;
mod thread_acceleration;
mod u32_hasher_types;
use u32_hasher_types::{u32hashmap_with_capacity, U32HasherState};
@@ -74,10 +76,11 @@ impl GbtGenerator {
///
/// Rejects if the thread panics or if the Mutex is poisoned.
#[napi]
pub async fn make(&self, mempool: Vec<ThreadTransaction>, max_uid: u32) -> Result<GbtResult> {
pub async fn make(&self, mempool: Vec<ThreadTransaction>, accelerations: Vec<ThreadAcceleration>, max_uid: u32) -> Result<GbtResult> {
trace!("make: Current State {:#?}", self.thread_transactions);
run_task(
Arc::clone(&self.thread_transactions),
accelerations,
max_uid as usize,
move |map| {
for tx in mempool {
@@ -96,11 +99,13 @@ impl GbtGenerator {
&self,
new_txs: Vec<ThreadTransaction>,
remove_txs: Vec<u32>,
accelerations: Vec<ThreadAcceleration>,
max_uid: u32,
) -> Result<GbtResult> {
trace!("update: Current State {:#?}", self.thread_transactions);
run_task(
Arc::clone(&self.thread_transactions),
accelerations,
max_uid as usize,
move |map| {
for tx in new_txs {
@@ -141,6 +146,7 @@ pub struct GbtResult {
/// to the `HashMap` as the only argument. (A move closure is recommended to meet the bounds)
async fn run_task<F>(
thread_transactions: Arc<Mutex<ThreadTransactionsMap>>,
accelerations: Vec<ThreadAcceleration>,
max_uid: usize,
callback: F,
) -> Result<GbtResult>
@@ -159,7 +165,7 @@ where
callback(&mut map);
info!("Starting gbt algorithm for {} elements...", map.len());
let result = gbt::gbt(&mut map, max_uid);
let result = gbt::gbt(&mut map, &accelerations, max_uid);
info!("Finished gbt algorithm for {} elements...", map.len());
debug!(

View File

@@ -0,0 +1,8 @@
use napi_derive::napi;
#[derive(Debug)]
#[napi(object)]
pub struct ThreadAcceleration {
pub uid: u32,
pub delta: f64, // fee delta
}

View File

@@ -23,8 +23,8 @@
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
"INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"AUDIT": true,
"ADVANCED_GBT_AUDIT": true,
"ADVANCED_GBT_MEMPOOL": true,
@@ -33,7 +33,8 @@
"MAX_BLOCKS_BULK_QUERY": 999,
"DISK_CACHE_BLOCK_INTERVAL": 999,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
@@ -50,7 +51,8 @@
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"RETRY_UNIX_SOCKET_AFTER": 888
"RETRY_UNIX_SOCKET_AFTER": 888,
"FALLBACK": []
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
@@ -92,10 +94,6 @@
"USERNAME": "__SOCKS5PROXY_USERNAME__",
"PASSWORD": "__SOCKS5PROXY_PASSWORD__"
},
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
@@ -129,6 +127,10 @@
"AUDIT_START_HEIGHT": 774000,
"SERVERS": []
},
"MEMPOOL_SERVICES": {
"API": "",
"ACCELERATIONS": false
},
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock"

View File

@@ -0,0 +1,24 @@
import { Common } from '../../api/common';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
const randomTransactions = require('./test-data/transactions-random.json');
const replacedTransactions = require('./test-data/transactions-replaced.json');
const rbfTransactions = require('./test-data/transactions-rbfs.json');
describe('Mempool Utils', () => {
test('should detect RBF transactions with fast method', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
});
test.only('should detect RBF transactions with scalable method', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
});
});

View File

@@ -1,4 +1,8 @@
import { calcDifficultyAdjustment, DifficultyAdjustment } from '../../api/difficulty-adjustment';
import {
calcBitsDifference,
calcDifficultyAdjustment,
DifficultyAdjustment,
} from '../../api/difficulty-adjustment';
describe('Mempool Difficulty Adjustment', () => {
test('should calculate Difficulty Adjustments properly', () => {
@@ -86,4 +90,46 @@ describe('Mempool Difficulty Adjustment', () => {
expect(result).toStrictEqual(vector[1]);
}
});
test('should calculate Difficulty change from bits fields of two blocks', () => {
// Check same exponent + check min max for output
expect(calcBitsDifference(0x1d000200, 0x1d000100)).toEqual(100);
expect(calcBitsDifference(0x1d000400, 0x1d000100)).toEqual(300);
expect(calcBitsDifference(0x1d000800, 0x1d000100)).toEqual(300); // Actually 700
expect(calcBitsDifference(0x1d000100, 0x1d000200)).toEqual(-50);
expect(calcBitsDifference(0x1d000100, 0x1d000400)).toEqual(-75);
expect(calcBitsDifference(0x1d000100, 0x1d000800)).toEqual(-75); // Actually -87.5
// Check new higher exponent
expect(calcBitsDifference(0x1c000200, 0x1d000001)).toEqual(100);
expect(calcBitsDifference(0x1c000400, 0x1d000001)).toEqual(300);
expect(calcBitsDifference(0x1c000800, 0x1d000001)).toEqual(300);
expect(calcBitsDifference(0x1c000100, 0x1d000002)).toEqual(-50);
expect(calcBitsDifference(0x1c000100, 0x1d000004)).toEqual(-75);
expect(calcBitsDifference(0x1c000100, 0x1d000008)).toEqual(-75);
// Check new lower exponent
expect(calcBitsDifference(0x1d000002, 0x1c000100)).toEqual(100);
expect(calcBitsDifference(0x1d000004, 0x1c000100)).toEqual(300);
expect(calcBitsDifference(0x1d000008, 0x1c000100)).toEqual(300);
expect(calcBitsDifference(0x1d000001, 0x1c000200)).toEqual(-50);
expect(calcBitsDifference(0x1d000001, 0x1c000400)).toEqual(-75);
expect(calcBitsDifference(0x1d000001, 0x1c000800)).toEqual(-75);
// Check error when exponents are too far apart
expect(() => calcBitsDifference(0x1d000001, 0x1a000800)).toThrow(
/Impossible exponent difference/
);
// Check invalid inputs
expect(() => calcBitsDifference(0x7f000001, 0x1a000800)).toThrow(
/Invalid bits/
);
expect(() => calcBitsDifference(0, 0x1a000800)).toThrow(/Invalid bits/);
expect(() => calcBitsDifference(100.2783, 0x1a000800)).toThrow(
/Invalid bits/
);
expect(() => calcBitsDifference(0x00800000, 0x1a000800)).toThrow(
/Invalid bits/
);
expect(() => calcBitsDifference(0x1c000000, 0x1a000800)).toThrow(
/Invalid bits/
);
});
});

View File

@@ -0,0 +1,277 @@
[
{
"txid": "13f007241d78e8b0b4e57d2ae3fd37bcfe3226534d7cadeba5a549860d960db0",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "cb8f206f4e88bec97107089f3e9e61d50cde53d4541992ae19759b71103cf75c",
"vout": 0,
"prevout": {
"scriptpubkey": "0014fd6d15ff832c12f1ff04a5ccd5039f7227b260bd",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 fd6d15ff832c12f1ff04a5ccd5039f7227b260bd",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1ql4k3tlur9sf0rlcy5hxd2qulwgnmyc9akehvth",
"value": 610677
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304302205c430b36ebd2bb327951d83440af1f58f127871b2baada4c4dde2bc0b6721f56021f3445099f1a40e35baeda32e8e3727b505ffba0d882b11f498c7762f4184e9901",
"0236b5edd4fbbcfb045960e42ec8a9968944084785932e32940e8cd2583b37da67"
],
"is_coinbase": false,
"sequence": 2147483648
}
],
"vout": [
{
"scriptpubkey": "76a9149d32ef812385f3811634e0c0117dd153a5de10a488ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 9d32ef812385f3811634e0c0117dd153a5de10a4 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1FLC7Bag7okAkKPCyZbgZZg3Hh1EuGZ5Rd",
"value": 344697
},
{
"scriptpubkey": "00147dee8a7a38abbfb00dbfba365c8d6712934cc491",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 7dee8a7a38abbfb00dbfba365c8d6712934cc491",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q0hhg573c4wlmqrdlhgm9ert8z2f5e3y3lf9hvx",
"value": 265396
}
],
"size": 224,
"weight": 572,
"fee": 584,
"status": {
"confirmed": false
},
"order": 2953680397,
"vsize": 143,
"adjustedVsize": 143,
"sigops": 5,
"feePerVsize": 4.083916083916084,
"adjustedFeePerVsize": 4.083916083916084,
"effectiveFeePerVsize": 4.083916083916084,
"firstSeen": 1691222538,
"uid": 526973,
"inputs": [
526728
],
"position": {
"block": 7,
"vsize": 21429708.5
},
"bestDescendant": null,
"cpfpChecked": true
},
{
"txid": "8e89b20f8a7fadb0e4cdbe57a00eee224f5076bac5387fc276916724e7c4a16a",
"version": 2,
"locktime": 800571,
"vin": [
{
"txid": "35e16762459539f3a8e52c5dee6a9ccaa9e9268efed33aa2c6e1b7805e849f24",
"vout": 0,
"prevout": {
"scriptpubkey": "0014d4f16ef275b3e1c4a4ecbef55a164933e0f6460f",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 d4f16ef275b3e1c4a4ecbef55a164933e0f6460f",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q6nckaun4k0suff8vhm6459jfx0s0v3s0ff4ukl",
"value": 1528924
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022019008b26e885bb43da25a11ffac147a057722072eedb68411f114f6e7eb82ebc02201b618264bb97756b88fc3bbc365b73044ac18b33b1067e31cfd5bcd0f50ed2c701",
"039b71145070bd3e8af28e27fa577f2e12ab6bb4e212d3eeaef08b4bc39e8cbc13"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "67c27ed0f767526234bcd5f795a31fab8ec4d0251bf12c68f2746951f4110d90",
"vout": 3,
"prevout": {
"scriptpubkey": "0014a7c3d613b321375054b2ac9b6114367bc034ad6f",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a7c3d613b321375054b2ac9b6114367bc034ad6f",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q5lpavyanyym4q49j4jdkz9pk00qrftt0yqzvk3",
"value": 436523
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402204e67285fc656bc45ed082499b076d5dba2fa21d0d7e64a0ae52b19d69a11760002200f037d81ee540b74397844513b72b08ed92b06db76bd20b08f7a0a3b36ab13d501",
"02a3ebae85f0225b6fbb5ff060afce683a4683507a57544605a29ee7d287e591b4"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "21c38fb9a2521e438c614f53b19ddd7a5594bcc4b77480e762fd4b702fad3374",
"vout": 1,
"prevout": {
"scriptpubkey": "00149660e34ef88106536c816c037b5b28dd64a812e2",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 9660e34ef88106536c816c037b5b28dd64a812e2",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qjeswxnhcsyr9xmypdsphkkegm4j2syhztgzxv4",
"value": 758149
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022021b556f0aa99329076bcc435338aceaf534963efcab306931b1b2b0461e16e0c02203a78942a3745c4da656bddfd8cf16b85dc04d652904e88682127cdd9ca63339001",
"0298963be4a8f66aca9fcf1c6dc95547aeaa82347543190c91e094c2321142b9f0"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "aa998dbae65240a7386bf7d468459551d99c3de8e2f9057ff5f2d38e17daf788",
"vout": 0,
"prevout": {
"scriptpubkey": "00147bb7413a39943b21ded98ad5e6ad7a222d273e17",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 7bb7413a39943b21ded98ad5e6ad7a222d273e17",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q0wm5zw3ejsajrhke3t27dtt6ygkjw0sh9lltg6",
"value": 1067200
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402205e2269f7d4ee0513b34354c38e920aef2dabac6f4350afb2dd105ff3ee43ae7b02202870322f2cb85cb0b2b0e38152f018bfff271dc3ec5aed0515854d0b259aaf3d01",
"03b87320cf3263a644a0d3f89c1b4a7304d9dfda9eb8c891560716abcb73e88b99"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "230253d195d779d4688ba16993985cd27b2e7a687d8b889b3bc63f19ece36f20",
"vout": 0,
"prevout": {
"scriptpubkey": "001439647bd997819d12dfc72b0fb9ff9ffcb84946f8",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 39647bd997819d12dfc72b0fb9ff9ffcb84946f8",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q89j8hkvhsxw39h789v8mnlulljuyj3hc9zve97",
"value": 361950
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402204f7ca868bb9b92a07fecdc6b9dd56e4e1d007ca1405952f98ed6bc416345b5f2022055320a97791417abf6628fcf6513ac5785b06c630f854d8595e96ea06c3841d301",
"03a3ffe8e3ef2eea129b227e9658164bae0a6d21c17da6de9973ba34d9e04b21a0"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "670771e265a0b62dbd3c1fec2b865177eaf0bafd0ae49dd40a1c9fcd9a847a81",
"vout": 0,
"prevout": {
"scriptpubkey": "0014d45d1b0022c7387e42c5452ced561bdb8fd4b521",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 d45d1b0022c7387e42c5452ced561bdb8fd4b521",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q63w3kqpzcuu8usk9g5kw64smmw8afdfpxmc2m0",
"value": 453275
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022071312921800441903b2099e723add8702dd0f92ec11526ff87acf6967ec64cbd02203deabe7ed56d5daaa9a95c5a607b1ab705ff1c46bc6984a6dca120e63a91768601",
"0257302ac8d9c4c8f9b1744f19bb432359326b9cc7bdddeeab9202749a6d92be58"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "0af82159eee2b69242f2ff032636e410b67ec1ace52e55fb0d20ed814cd64803",
"vout": 0,
"prevout": {
"scriptpubkey": "001459e4d6bfefc6b45f955a69c4aeca26348e9d54ed",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 59e4d6bfefc6b45f955a69c4aeca26348e9d54ed",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qt8jdd0l0c669l926d8z2aj3xxj8f648dtyn7tc",
"value": 439961
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022027540322e92c23c5513aa2587e7feb56a8ce82f879269d6b3cbd425634b44f8e022045572dee7262b02130bfe32d8aa8abbfaa64e101abfc819bba5380c78876692d01",
"03fe02262d87f4a5289d3dd66e3d9a74cd49fa1cad0249284a7451896a827249a5"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "68cf9c784870a4f888f044755f7ce318557f652461db8ef887d279672f186018",
"vout": 0,
"prevout": {
"scriptpubkey": "001454822b2d5d52597a78b630921cf439a41e32f2f9",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 54822b2d5d52597a78b630921cf439a41e32f2f9",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q2jpzkt2a2fvh579kxzfpeape5s0r9uhewhl5n4",
"value": 227639
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402203ad511d6a8730748b8828bc38897d360451adf620ebdc1d229c08c097c80bef202202f50c793d95b5200cf2258e03896a3be7720df0eb3b8c810c86db74341a7e83e01",
"0294992e9f4546e6e119741f908411ae531e9d1ff732d69b4dff8172aaf2a4b216"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "793f01dfdb19bf41f958fd917c16d9c4dd5d5e1a5c0434bfdb367212659d1b5b",
"vout": 0,
"prevout": {
"scriptpubkey": "0014f54edf8ae647b5300e2674523254e923d93d169f",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 f54edf8ae647b5300e2674523254e923d93d169f",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q748dlzhxg76nqr3xw3fry48fy0vn695lvhlkxv",
"value": 227070
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402206e807ab616f4f2887ba703ae744d856142d9aca8128698419bbb67fb4fad8177022060fc65c7cd66baa88ad1e1d317a6edd5f6cb52fe8bff6e5405ffa1acf9d945d901",
"02a0ad0167c6e9edf62677404d74d3b80ea276e47e758ffaa6ca17bd65ac79f7aa"
],
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "00148a5c45ccfc29d209940d94525e2edb7743a1ad8a",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 8a5c45ccfc29d209940d94525e2edb7743a1ad8a",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q3fwytn8u98fqn9qdj3f9utkmwap6rtv2ym33zm",
"value": 5500000
}
],
"size": 1375,
"weight": 2605,
"fee": 691,
"status": {
"confirmed": false
},
"order": 1788986599,
"vsize": 651,
"adjustedVsize": 651.25,
"sigops": 9,
"feePerVsize": 1.0610364683301343,
"adjustedFeePerVsize": 1.0610364683301343,
"effectiveFeePerVsize": 1.0610364683301343,
"firstSeen": 1691163298,
"uid": 120494,
"inputs": [],
"position": {
"block": 7,
"vsize": 93780091.5
},
"bestDescendant": null,
"cpfpChecked": true
}
]

View File

@@ -0,0 +1,121 @@
[
{
"txid": "7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "d863deb706de5a611028f7547e16ea81d7819e44beb640fb30a9ba30c585140f",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ",
"value": 799995000
},
"scriptsig": "483045022100aeeddfb9785c5a4b70e90d0445785c68b7a44e28853441134a70ddc4da39527602203dfe1ec1a377aaacb64ae65c7c944caf1398d2dc063f712251b4cf696d44d3cb01210314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6",
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100aeeddfb9785c5a4b70e90d0445785c68b7a44e28853441134a70ddc4da39527602203dfe1ec1a377aaacb64ae65c7c944caf1398d2dc063f712251b4cf696d44d3cb01 OP_PUSHBYTES_33 0314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6",
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "6a4c5058325b8669baa9259e082f064005bc92274b559337ac317798f5d76f2d0577ed5a96042fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b",
"scriptpubkey_asm": "OP_RETURN OP_PUSHDATA1 58325b8669baa9259e082f064005bc92274b559337ac317798f5d76f2d0577ed5a96042fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b",
"scriptpubkey_type": "op_return",
"value": 0
},
{
"scriptpubkey": "a9144890aae025c84cb72a9730b49ca12595d6f6088d87",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 4890aae025c84cb72a9730b49ca12595d6f6088d OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "38Jht2bzmJL4EwoFvvyFzejhfEb4J7KxLb",
"value": 155000
},
{
"scriptpubkey": "76a91486e7dad6617303942a448b7f8afe9653e5624a5e88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 86e7dad6617303942a448b7f8afe9653e5624a5e OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1DJKJGApgX4W8BSQ8FRPLqX78UaCskT4r2",
"value": 155000
},
{
"scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ",
"value": 799675549
}
],
"size": 350,
"weight": 1400,
"fee": 9451,
"status": {
"confirmed": false
},
"order": 2798688215,
"vsize": 350,
"adjustedVsize": 350,
"sigops": 8,
"feePerVsize": 27.002857142857142,
"adjustedFeePerVsize": 27.002857142857142,
"effectiveFeePerVsize": 27.002857142857142,
"firstSeen": 1691218536,
"uid": 513598,
"inputs": [],
"position": {
"block": 0,
"vsize": 22166
},
"bestDescendant": null,
"cpfpChecked": true
},
{
"txid": "5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "b50225a04a1d6fbbfa7a2122bc0580396f614027b3957f476229633576f06130",
"vout": 0,
"prevout": {
"scriptpubkey": "0014a24f913f8a9c30a4c302c2c78f2fd7addb08fd07",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a24f913f8a9c30a4c302c2c78f2fd7addb08fd07",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q5f8ez0u2nsc2fsczctrc7t7h4hds3lg82ewqhz",
"value": 612917
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3045022100a0c23953ace5d022b7a6d45d1ae1730bf20a4d594bb5d4fa7aa80e4881b44d320220008f9b144805bb91995fc0f452a56e09f4ad16fa149d71ae9b5d57c742e8e2cc01",
"03dc2c7b687019b40a68d713322675206cc266e34e5340ec982c13ff0222c3b2b6"
],
"is_coinbase": false,
"sequence": 2147483649
}
],
"vout": [
{
"scriptpubkey": "0014199a98f9589364ffe5ef5bbae45ce5dfcbb873bd",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 199a98f9589364ffe5ef5bbae45ce5dfcbb873bd",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qrxdf372cjdj0le00twawgh89ml9msuaau62gk4",
"value": 611909
}
],
"size": 192,
"weight": 438,
"fee": 1008,
"status": {
"confirmed": false
},
"bestDescendant": null,
"descendants": null,
"adjustedFeePerVsize": 10.2283,
"sigops": 1,
"adjustedVsize": 109.5
}
]

View File

@@ -0,0 +1,139 @@
[
{
"txid": "008592364e21c1e3d62ba9538ac78a81779897b52100af5707ab063df98964f2",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "d863deb706de5a611028f7547e16ea81d7819e44beb640fb30a9ba30c585140f",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ",
"value": 799995000
},
"scriptsig": "483045022100c1fb331d155a7d299a0451d14fa1122b328e0e239afc9ba8dc2aff449ddc5a3a02201c1e19030d1efa432f5069cd369d7ad09a67f68501345e4db35f7b799605f55601210314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6",
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100c1fb331d155a7d299a0451d14fa1122b328e0e239afc9ba8dc2aff449ddc5a3a02201c1e19030d1efa432f5069cd369d7ad09a67f68501345e4db35f7b799605f55601 OP_PUSHBYTES_33 0314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6",
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "6a4c5058325b78064160b631b5a15d9078d99c0db066449fb4c59bbfa4d987ba906e2990088b2fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b",
"scriptpubkey_asm": "OP_RETURN OP_PUSHDATA1 58325b78064160b631b5a15d9078d99c0db066449fb4c59bbfa4d987ba906e2990088b2fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b",
"scriptpubkey_type": "op_return",
"value": 0
},
{
"scriptpubkey": "a9144890aae025c84cb72a9730b49ca12595d6f6088d87",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 4890aae025c84cb72a9730b49ca12595d6f6088d OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "38Jht2bzmJL4EwoFvvyFzejhfEb4J7KxLb",
"value": 155000
},
{
"scriptpubkey": "76a91486e7dad6617303942a448b7f8afe9653e5624a5e88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 86e7dad6617303942a448b7f8afe9653e5624a5e OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1DJKJGApgX4W8BSQ8FRPLqX78UaCskT4r2",
"value": 155000
},
{
"scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ",
"value": 799676250
}
],
"size": 350,
"weight": 1400,
"fee": 8750,
"status": {
"confirmed": false
},
"order": 4066675193,
"vsize": 350,
"adjustedVsize": 350,
"sigops": 8,
"feePerVsize": 25,
"adjustedFeePerVsize": 25,
"effectiveFeePerVsize": 25,
"firstSeen": 1691218516,
"uid": 512584,
"inputs": [],
"position": {
"block": 0,
"vsize": 13846
},
"bestDescendant": null,
"cpfpChecked": true
},
{
"txid": "b7981a624e4261c11f1246314d41e74be56af82eb557bcd054a5e0f94c023668",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "b50225a04a1d6fbbfa7a2122bc0580396f614027b3957f476229633576f06130",
"vout": 0,
"prevout": {
"scriptpubkey": "0014a24f913f8a9c30a4c302c2c78f2fd7addb08fd07",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a24f913f8a9c30a4c302c2c78f2fd7addb08fd07",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q5f8ez0u2nsc2fsczctrc7t7h4hds3lg82ewqhz",
"value": 612917
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402204dd10f14afa41bc76d8278140ff1ec3d3f87f2c207bbb5418cc76dab30d7f6a402207877cc9c6a2c724b6ea7a1c24ac00022469f194fd1a4bd8030bbca1787d3f5f301",
"03dc2c7b687019b40a68d713322675206cc266e34e5340ec982c13ff0222c3b2b6"
],
"is_coinbase": false,
"sequence": 2147483648
}
],
"vout": [
{
"scriptpubkey": "76a9149d32ef812385f3811634e0c0117dd153a5de10a488ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 9d32ef812385f3811634e0c0117dd153a5de10a4 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1FLC7Bag7okAkKPCyZbgZZg3Hh1EuGZ5Rd",
"value": 344697
},
{
"scriptpubkey": "00144c2671336ca8761863b4c68d64d4672491fec1b9",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 4c2671336ca8761863b4c68d64d4672491fec1b9",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qfsn8zvmv4pmpsca5c6xkf4r8yjglasdesrawcx",
"value": 267636
}
],
"size": 225,
"weight": 573,
"fee": 584,
"status": {
"confirmed": false
},
"order": 1748369996,
"vsize": 143,
"adjustedVsize": 143.25,
"sigops": 5,
"feePerVsize": 4.076788830715532,
"adjustedFeePerVsize": 4.076788830715532,
"effectiveFeePerVsize": 4.076788830715532,
"firstSeen": 1691222376,
"uid": 526515,
"inputs": [],
"position": {
"block": 7,
"vsize": 22021095.5
},
"bestDescendant": null,
"cpfpChecked": true
}
]

View File

@@ -47,11 +47,17 @@ describe('Mempool Backend Config', () => {
DISK_CACHE_BLOCK_INTERVAL: 6,
MAX_PUSH_TX_SIZE_WEIGHT: 400000,
ALLOW_UNREACHABLE: true,
PRICE_UPDATES_PER_HOUR: 1,
});
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null, RETRY_UNIX_SOCKET_AFTER: 30000 });
expect(config.ESPLORA).toStrictEqual({
REST_API_URL: 'http://127.0.0.1:3000',
UNIX_SOCKET_PATH: null,
RETRY_UNIX_SOCKET_AFTER: 30000,
FALLBACK: [],
});
expect(config.CORE_RPC).toStrictEqual({
HOST: '127.0.0.1',
@@ -101,11 +107,6 @@ describe('Mempool Backend Config', () => {
PASSWORD: ''
});
expect(config.PRICE_DATA_SERVER).toStrictEqual({
TOR_URL: 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
CLEARNET_URL: 'https://price.bisq.wiz.biz/getAllMarketPrices'
});
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual({
MEMPOOL_API: 'https://mempool.space/api/v1',
MEMPOOL_ONION: 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
@@ -129,6 +130,11 @@ describe('Mempool Backend Config', () => {
SERVERS: []
});
expect(config.MEMPOOL_SERVICES).toStrictEqual({
API: "",
ACCELERATIONS: false,
});
expect(config.REDIS).toStrictEqual({
ENABLED: false,
UNIX_SOCKET_PATH: ''
@@ -163,10 +169,10 @@ describe('Mempool Backend Config', () => {
expect(config.SOCKS5PROXY).toStrictEqual(fixture.SOCKS5PROXY);
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
expect(config.REDIS).toStrictEqual(fixture.REDIS);
});
});
@@ -180,41 +186,50 @@ describe('Mempool Backend Config', () => {
for (const [key, value] of Object.entries(jsonObj)) {
// We have a few cases where we can't follow the pattern
if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
console.log('skipping check for MEMPOOL_HTTP_PORT');
if (process.env.CI) {
console.log('skipping check for MEMPOOL_HTTP_PORT');
}
continue;
}
switch (typeof value) {
case 'object': {
if (Array.isArray(value)) {
continue;
} else {
parseJson(value, key);
}
break;
}
default: {
if (root) {
//The flattened string, i.e, __MEMPOOL_ENABLED__
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
//The string used as the environment variable, i.e, MEMPOOL_ENABLED
const envVarStr = `${root ? root : ''}_${key}`;
let defaultEntry;
//The string used as the default value, to be checked as a regex, i.e, __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=(.*)}
const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
console.log(`looking for ${defaultEntry} in the start.sh script`);
const re = new RegExp(defaultEntry);
expect(startSh).toMatch(re);
if (Array.isArray(value)) {
defaultEntry = `${replaceStr}=\${${envVarStr}:=[]}`;
if (process.env.CI) {
console.log(`looking for ${defaultEntry} in the start.sh script`);
}
//Regex matching does not work with the array values
expect(startSh).toContain(defaultEntry);
} else {
defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
if (process.env.CI) {
console.log(`looking for ${defaultEntry} in the start.sh script`);
}
const re = new RegExp(defaultEntry);
expect(startSh).toMatch(re);
}
//The string that actually replaces the values in the config file
const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json';
console.log(`looking for ${sedStr} in the start.sh script`);
if (process.env.CI) {
console.log(`looking for ${sedStr} in the start.sh script`);
}
expect(startSh).toContain(sedStr);
break;
}
else {
parseJson(value, key);
}
}
}
parseJson(fixture);
});
});

View File

@@ -1,5 +1,5 @@
import fs from 'fs';
import { GbtGenerator, ThreadTransaction } from '../../../rust-gbt';
import { GbtGenerator, ThreadTransaction } from 'rust-gbt';
import path from 'path';
const baseline = require('./test-data/target-template.json');
@@ -15,7 +15,7 @@ describe('Rust GBT', () => {
test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => {
const rustGbt = new GbtGenerator();
const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer);
const result = await rustGbt.make(mempool, maxUid);
const result = await rustGbt.make(mempool, [], maxUid);
const blocks: [string, number][][] = result.blocks.map(block => {
return block.map(uid => [vectorUidMap.get(uid) || 'missing', uid]);

View File

@@ -6,16 +6,17 @@ import rbfCache from './rbf-cache';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], score: 0, similarity: 1 };
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 0, similarity: 1 };
}
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse
const isDisplaced = {};
let displacedWeight = 0;
@@ -28,6 +29,9 @@ class Audit {
const now = Math.round((Date.now() / 1000));
for (const tx of transactions) {
inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid);
}
}
// coinbase is always expected
if (transactions[0]) {
@@ -149,6 +153,7 @@ class Audit {
fresh,
sigop: [],
fullrbf: rbf,
accelerated,
score,
similarity,
};

View File

@@ -3,7 +3,8 @@ import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getMempoolTransactions(lastTxid: string);
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getAllMempoolTransactions(lastTxid: string);
$getTransactionHex(txId: string): Promise<string>;
$getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>;
@@ -22,6 +23,8 @@ export interface AbstractBitcoinApi {
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
startHealthChecks(): void;
}
export interface BitcoinRpcCredentials {
host: string;

View File

@@ -60,8 +60,13 @@ class BitcoinApi implements AbstractBitcoinApi {
});
}
$getMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
return Promise.resolve([]);
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
}
$getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
}
async $getTransactionHex(txId: string): Promise<string> {
@@ -350,6 +355,7 @@ class BitcoinApi implements AbstractBitcoinApi {
return transaction;
}
public startHealthChecks(): void {};
}
export default BitcoinApi;

View File

@@ -214,6 +214,7 @@ class BitcoinRoutes {
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
sigops: tx.sigops,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
});
return;
}

View File

@@ -1,112 +1,260 @@
import config from '../../config';
import axios, { AxiosRequestConfig } from 'axios';
import axios, { AxiosResponse } from 'axios';
import http from 'http';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
const axiosConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true, })
});
interface FailoverHost {
host: string,
rtts: number[],
rtt: number
failures: number,
socket?: boolean,
outOfSync?: boolean,
unreachable?: boolean,
preferred?: boolean,
}
class ElectrsApi implements AbstractBitcoinApi {
private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? {
socketPath: config.ESPLORA.UNIX_SOCKET_PATH,
timeout: 10000,
} : {
timeout: 10000,
};
private axiosConfigTcpSocketOnly: AxiosRequestConfig = {
timeout: 10000,
};
unixSocketRetryTimeout;
activeAxiosConfig;
class FailoverRouter {
activeHost: FailoverHost;
fallbackHost: FailoverHost;
hosts: FailoverHost[];
multihost: boolean;
pollInterval: number = 60000;
pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create();
requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true })
});
constructor() {
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
// setup list of hosts
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return {
host: domain,
rtts: [],
rtt: Infinity,
failures: 0,
};
});
this.activeHost = {
host: config.ESPLORA.UNIX_SOCKET_PATH || config.ESPLORA.REST_API_URL,
rtts: [],
rtt: 0,
failures: 0,
socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true,
};
this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost);
this.multihost = this.hosts.length > 1;
}
fallbackToTcpSocket() {
if (!this.unixSocketRetryTimeout) {
logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`);
// Retry the unix socket after a few seconds
this.unixSocketRetryTimeout = setTimeout(() => {
logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`);
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
this.unixSocketRetryTimeout = undefined;
}, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER);
public startHealthChecks(): void {
// use axios interceptors to measure request rtt
this.pollConnection.interceptors.request.use((config) => {
config['meta'] = { startTime: Date.now() };
return config;
});
this.pollConnection.interceptors.response.use((response) => {
response.config['meta'].rtt = Date.now() - response.config['meta'].startTime;
return response;
});
if (this.multihost) {
this.pollHosts();
}
}
// start polling hosts to measure availability & rtt
private async pollHosts(): Promise<void> {
if (this.pollTimer) {
clearTimeout(this.pollTimer);
}
// Use the TCP socket (reach a different esplora instance through nginx)
this.activeAxiosConfig = this.axiosConfigTcpSocketOnly;
const results = await Promise.allSettled(this.hosts.map(async (host) => {
if (host.socket) {
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 5000 });
} else {
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 5000 });
}
}));
const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0);
// update rtts & sync status
for (let i = 0; i < results.length; i++) {
const host = this.hosts[i];
const result = results[i].status === 'fulfilled' ? (results[i] as PromiseFulfilledResult<AxiosResponse<number, any>>).value : null;
if (result) {
const height = result.data;
const rtt = result.config['meta'].rtt;
host.rtts.unshift(rtt);
host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
if (height == null || isNaN(height) || (maxHeight - height > 2)) {
host.outOfSync = true;
} else {
host.outOfSync = false;
}
host.unreachable = false;
} else {
host.unreachable = true;
}
}
this.sortHosts();
logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`);
// switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) {
if (this.activeHost.unreachable) {
logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`);
} else if (this.activeHost.outOfSync) {
logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`);
} else {
logger.debug(`${this.activeHost.host} is no longer the best esplora host`);
}
this.electHost();
}
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
}
$queryWrapper<T>(url, responseType = 'json'): Promise<T> {
return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType })
.then((response) => response.data)
// sort hosts by connection quality, and update default fallback
private sortHosts(): void {
// sort by connection quality
this.hosts.sort((a, b) => {
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
if (a.preferred === b.preferred) {
// lower rtt is best
return a.rtt - b.rtt;
} else { // unless we have a preferred host
return a.preferred ? -1 : 1;
}
} else { // or the host is out of sync
return (a.unreachable || a.outOfSync) ? 1 : -1;
}
});
if (this.hosts.length > 1 && this.hosts[0] === this.activeHost) {
this.fallbackHost = this.hosts[1];
} else {
this.fallbackHost = this.hosts[0];
}
}
// depose the active host and choose the next best replacement
private electHost(): void {
this.activeHost.outOfSync = true;
this.activeHost.failures = 0;
this.sortHosts();
this.activeHost = this.hosts[0];
logger.warn(`Switching esplora host to ${this.activeHost.host}`);
}
private addFailure(host: FailoverHost): FailoverHost {
host.failures++;
if (host.failures > 5 && this.multihost) {
logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`);
this.electHost();
return this.activeHost;
} else {
return this.fallbackHost;
}
}
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
let axiosConfig;
let url;
if (host.socket) {
axiosConfig = { socketPath: host.host, timeout: 10000, responseType };
url = path;
} else {
axiosConfig = { timeout: 10000, responseType };
url = host.host + path;
}
return (method === 'post'
? this.requestConnection.post<T>(url, data, axiosConfig)
: this.requestConnection.get<T>(url, axiosConfig)
).then((response) => { host.failures = Math.max(0, host.failures - 1); return response.data; })
.catch((e) => {
if (e?.code === 'ECONNREFUSED') {
this.fallbackToTcpSocket();
let fallbackHost = this.fallbackHost;
if (e?.response?.status !== 404) {
logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`);
fallbackHost = this.addFailure(host);
}
if (retry && e?.code === 'ECONNREFUSED' && this.multihost) {
// Retry immediately
return axiosConnection.get<T>(url, this.activeAxiosConfig)
.then((response) => response.data)
.catch((e) => {
logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`);
throw e;
});
return this.$query(method, path, data, responseType, fallbackHost, false);
} else {
throw e;
}
});
}
public async $get<T>(path, responseType = 'json'): Promise<T> {
return this.$query<T>('get', path, null, responseType);
}
public async $post<T>(path, data: any, responseType = 'json'): Promise<T> {
return this.$query<T>('post', path, data, responseType);
}
}
class ElectrsApi implements AbstractBitcoinApi {
private failoverRouter = new FailoverRouter();
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids');
return this.failoverRouter.$get<IEsploraApi.Transaction['txid'][]>('/mempool/txids');
}
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txId);
}
async $getMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/mempool/txs', txids, 'json');
}
async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
}
$getTransactionHex(txId: string): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
return this.failoverRouter.$get<string>('/tx/' + txId + '/hex');
}
$getBlockHeightTip(): Promise<number> {
return this.$queryWrapper<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height');
return this.failoverRouter.$get<number>('/blocks/tip/height');
}
$getBlockHashTip(): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash');
return this.failoverRouter.$get<string>('/blocks/tip/hash');
}
$getTxIdsForBlock(hash: string): Promise<string[]> {
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
return this.failoverRouter.$get<string[]>('/block/' + hash + '/txids');
}
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs');
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/block/' + hash + '/txs');
}
$getBlockHash(height: number): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
return this.failoverRouter.$get<string>('/block-height/' + height);
}
$getBlockHeader(hash: string): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header');
return this.failoverRouter.$get<string>('/block/' + hash + '/header');
}
$getBlock(hash: string): Promise<IEsploraApi.Block> {
return this.$queryWrapper<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash);
return this.failoverRouter.$get<IEsploraApi.Block>('/block/' + hash);
}
$getRawBlock(hash: string): Promise<Buffer> {
return this.$queryWrapper<any>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer')
return this.failoverRouter.$get<any>('/block/' + hash + '/raw', 'arraybuffer')
.then((response) => { return Buffer.from(response.data); });
}
@@ -135,11 +283,11 @@ class ElectrsApi implements AbstractBitcoinApi {
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.$queryWrapper<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout);
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
}
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
return this.$queryWrapper<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends');
return this.failoverRouter.$get<IEsploraApi.Outspend[]>('/tx/' + txId + '/outspends');
}
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
@@ -150,6 +298,10 @@ class ElectrsApi implements AbstractBitcoinApi {
}
return outspends;
}
public startHealthChecks(): void {
this.failoverRouter.startHealthChecks();
}
}
export default ElectrsApi;

View File

@@ -28,12 +28,13 @@ import chainTips from './chain-tips';
import websocketHandler from './websocket-handler';
import redisCache from './redis-cache';
import rbfCache from './rbf-cache';
import { calcBitsDifference } from './difficulty-adjustment';
class Blocks {
private blocks: BlockExtended[] = [];
private blockSummaries: BlockSummary[] = [];
private currentBlockHeight = 0;
private currentDifficulty = 0;
private currentBits = 0;
private lastDifficultyAdjustmentTime = 0;
private previousDifficultyRetarget = 0;
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
@@ -666,14 +667,18 @@ class Blocks {
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentDifficulty = block.difficulty;
this.currentBits = block.bits;
if (blockHeightTip >= 2016) {
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
const previousPeriodBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(previousPeriodBlockHash);
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
if (['liquid', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
this.previousDifficultyRetarget = NaN;
} else {
this.previousDifficultyRetarget = calcBitsDifference(previousPeriodBlock.bits, block.bits);
}
logger.debug(`Initial difficulty adjustment data set.`);
}
} else {
@@ -701,7 +706,7 @@ class Blocks {
// fill in missing transaction fee data from verboseBlock
for (let i = 0; i < transactions.length; i++) {
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
transactions[i].fee = (verboseBlock.tx[i].fee * 100_000_000) || 0;
}
}
@@ -782,18 +787,33 @@ class Blocks {
if (block.height % 2016 === 0) {
if (Common.indexingEnabled()) {
let adjustment;
if (['liquid', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
adjustment = NaN;
} else {
adjustment = Math.round(
// calcBitsDifference returns +- percentage, +100 returns to positive, /100 returns to ratio.
// Instead of actually doing /100, just reduce the multiplier.
(calcBitsDifference(this.currentBits, block.bits) + 100) * 10000
) / 1000000; // Remove float point noise
}
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.timestamp,
height: block.height,
difficulty: block.difficulty,
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
adjustment,
});
this.updateTimerProgress(timer, `saved difficulty adjustment for ${this.currentBlockHeight}`);
}
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
if (['liquid', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
this.previousDifficultyRetarget = NaN;
} else {
this.previousDifficultyRetarget = calcBitsDifference(this.currentBits, block.bits);
}
this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentDifficulty = block.difficulty;
this.currentBits = block.bits;
}
// wait for pending async callbacks to finish
@@ -941,7 +961,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
return {
txid: tx.txid,
fee: tx.fee,
fee: tx.fee || 0,
vsize: tx.vsize,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
rate: tx.effectiveFeePerVsize

View File

@@ -59,10 +59,12 @@ export class Common {
return arr;
}
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[]): { [txid: string]: MempoolTransactionExtended[] } {
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
added
.forEach((addedTx) => {
// For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
added.forEach((addedTx) => {
const foundMatches = deleted.filter((deletedTx) => {
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
return addedTx.fee > deletedTx.fee
@@ -73,9 +75,40 @@ export class Common {
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
});
if (foundMatches?.length) {
matches[addedTx.txid] = foundMatches;
matches[addedTx.txid] = [...new Set(foundMatches)];
}
});
} else {
// for large N, build a lookup table of prevouts we can check in ~constant time
const deletedSpendMap: { [txid: string]: { [vout: number]: MempoolTransactionExtended } } = {};
for (const tx of deleted) {
for (const vin of tx.vin) {
if (!deletedSpendMap[vin.txid]) {
deletedSpendMap[vin.txid] = {};
}
deletedSpendMap[vin.txid][vin.vout] = tx;
}
}
for (const addedTx of added) {
const foundMatches = new Set<MempoolTransactionExtended>();
for (const vin of addedTx.vin) {
const deletedTx = deletedSpendMap[vin.txid]?.[vin.vout];
if (deletedTx && deletedTx.txid !== addedTx.txid
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
&& addedTx.fee > deletedTx.fee
// The new transaction must pay more fee per kB than the replaced tx.
&& addedTx.adjustedFeePerVsize > deletedTx.adjustedFeePerVsize
) {
foundMatches.add(deletedTx);
}
if (foundMatches.size) {
matches[addedTx.txid] = [...foundMatches];
}
}
}
}
return matches;
}
@@ -108,9 +141,10 @@ export class Common {
static stripTransaction(tx: TransactionExtended): TransactionStripped {
return {
txid: tx.txid,
fee: tx.fee,
fee: tx.fee || 0,
vsize: tx.weight / 4,
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
acc: tx.acceleration || undefined,
rate: tx.effectiveFeePerVsize,
};
}
@@ -460,7 +494,7 @@ export class Common {
};
}
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string }[]): EffectiveFeeStats {
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats {
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
let weightCount = 0;

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 64;
private static currentVersion = 65;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -548,6 +548,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
await this.updateToSchemaVersion(64);
}
if (databaseSchemaVersion < 65 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(65);
}
}
/**

View File

@@ -16,6 +16,68 @@ export interface DifficultyAdjustment {
expectedBlocks: number; // Block count
}
/**
* Calculate the difficulty increase/decrease by using the `bits` integer contained in two
* block headers.
*
* Warning: Only compare `bits` from blocks in two adjacent difficulty periods. This code
* assumes the maximum difference is x4 or /4 (as per the protocol) and will throw an
* error if an exponent difference of 2 or more is seen.
*
* @param {number} oldBits The 32 bit `bits` integer from a block header.
* @param {number} newBits The 32 bit `bits` integer from a block header in the next difficulty period.
* @returns {number} A floating point decimal of the difficulty change from old to new.
* (ie. 21.3 means 21.3% increase in difficulty, -21.3 is a 21.3% decrease in difficulty)
*/
export function calcBitsDifference(oldBits: number, newBits: number): number {
// Must be
// - integer
// - highest exponent is 0x20, so max value (as integer) is 0x207fffff
// - min value is 1 (exponent = 0)
// - highest bit of the number-part is +- sign, it must not be 1
const verifyBits = (bits: number): void => {
if (
Math.floor(bits) !== bits ||
bits > 0x207fffff ||
bits < 1 ||
(bits & 0x00800000) !== 0 ||
(bits & 0x007fffff) === 0
) {
throw new Error('Invalid bits');
}
};
verifyBits(oldBits);
verifyBits(newBits);
// No need to mask exponents because we checked the bounds above
const oldExp = oldBits >> 24;
const newExp = newBits >> 24;
const oldNum = oldBits & 0x007fffff;
const newNum = newBits & 0x007fffff;
// The diff can only possibly be 1, 0, -1
// (because maximum difficulty change is x4 or /4 (2 bits up or down))
let result: number;
switch (newExp - oldExp) {
// New less than old, target lowered, difficulty increased
case -1:
result = ((oldNum << 8) * 100) / newNum - 100;
break;
// Same exponent, compare numbers as is.
case 0:
result = (oldNum * 100) / newNum - 100;
break;
// Old less than new, target raised, difficulty decreased
case 1:
result = (oldNum * 100) / (newNum << 8) - 100;
break;
default:
throw new Error('Impossible exponent difference');
}
// Min/Max values
return result > 300 ? 300 : result < -75 ? -75 : result;
}
export function calcDifficultyAdjustment(
DATime: number,
nowSeconds: number,

View File

@@ -42,6 +42,12 @@ class NodesRoutes {
switch (config.MEMPOOL.NETWORK) {
case 'testnet':
nodesList = [
'0259db43b4e4ac0ff12a805f2d81e521253ba2317f6739bc611d8e2fa156d64256',
'0352b9944b9a52bd2116c91f1ba70c4ef851ac5ba27e1b20f1d92da3ade010dd10',
'03424f5a7601eaa47482cb17100b31a84a04d14fb44b83a57eeceffd8e299878e3',
'032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470',
'022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421',
'02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680',
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
@@ -64,6 +70,12 @@ class NodesRoutes {
break;
case 'signet':
nodesList = [
'029fe3621fc0c6e08056a14b868f8fb9acca1aa28a129512f6cea0f0d7654d9f92',
'02f60cd7a3a4f1c953dd9554a6ebd51a34f8b10b8124b7fc43a0b381139b55c883',
'03cbbf581774700865eebd1be42d022bc004ba30881274ab304e088a25d70e773d',
'0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc',
'02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9',
'0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4',
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
@@ -86,6 +98,12 @@ class NodesRoutes {
break;
default:
nodesList = [
'02b12b889fe3c943cb05645921040ef13d6d397a2e7a4ad000e28500c505ff26d6',
'0302240ac9d71b39617cbde2764837ec3d6198bd6074b15b75d2ff33108e89d2e1',
'03364a8ace313376e5e4b68c954e287c6388e16df9e9fdbaf0363ecac41105cbf6',
'03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc',
'03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e',
'0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055',
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',

View File

@@ -1,10 +1,11 @@
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction } from '../../rust-gbt';
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
import logger from '../logger';
import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats } from '../mempool.interfaces';
import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag } from '../mempool.interfaces';
import { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config';
import { Worker } from 'worker_threads';
import path from 'path';
import mempool from './mempool';
const MAX_UINT32 = Math.pow(2, 32) - 1;
@@ -170,7 +171,7 @@ class MempoolBlocks {
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionStripped[] = [];
let removed: string[] = [];
const changed: { txid: string, rate: number | undefined }[] = [];
const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
@@ -192,8 +193,8 @@ class MempoolBlocks {
mempoolBlocks[i].transactions.forEach(tx => {
if (!prevIds[tx.txid]) {
added.push(tx);
} else if (tx.rate !== prevIds[tx.txid].rate) {
changed.push({ txid: tx.txid, rate: tx.rate });
} else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
}
});
}
@@ -206,14 +207,19 @@ class MempoolBlocks {
return mempoolBlockDeltas;
}
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now();
// reset mempool short ids
this.resetUids();
for (const tx of Object.values(newMempool)) {
this.setUid(tx);
if (saveResults) {
this.resetUids();
}
// set missing short ids
for (const tx of Object.values(newMempool)) {
this.setUid(tx, !saveResults);
}
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
// prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread
@@ -222,7 +228,7 @@ class MempoolBlocks {
if (entry.uid !== null && entry.uid !== undefined) {
const stripped = {
uid: entry.uid,
fee: entry.fee,
fee: entry.fee + (useAccelerations && (!accelerationPool || accelerations[entry.txid]?.pools?.includes(accelerationPool)) ? (accelerations[entry.txid]?.feeDelta || 0) : 0),
weight: (entry.adjustedVsize * 4),
sigops: entry.sigops,
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
@@ -262,7 +268,7 @@ class MempoolBlocks {
// clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener);
const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), saveResults);
const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), accelerations, accelerationPool, saveResults);
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
@@ -273,25 +279,29 @@ class MempoolBlocks {
return this.mempoolBlocks;
}
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], saveResults: boolean = false): Promise<void> {
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise<void> {
if (!this.txSelectionWorker) {
// need to reset the worker
await this.$makeBlockTemplates(newMempool, saveResults);
await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations);
return;
}
const start = Date.now();
for (const tx of Object.values(added)) {
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
const addedAndChanged: MempoolTransactionExtended[] = useAccelerations ? accelerationDelta.map(txid => newMempool[txid]).filter(tx => tx != null).concat(added) : added;
for (const tx of addedAndChanged) {
this.setUid(tx, true);
}
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
// prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread
const addedStripped: CompactThreadTransaction[] = added.filter(entry => (entry.uid !== null && entry.uid !== undefined)).map(entry => {
const addedStripped: CompactThreadTransaction[] = addedAndChanged.filter(entry => entry.uid != null).map(entry => {
return {
uid: entry.uid || 0,
fee: entry.fee,
fee: entry.fee + (useAccelerations ? (accelerations[entry.txid]?.feeDelta || 0) : 0),
weight: (entry.adjustedVsize * 4),
sigops: entry.sigops,
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
@@ -318,7 +328,7 @@ class MempoolBlocks {
// clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener);
this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), saveResults);
this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), accelerations, null, saveResults);
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
} catch (e) {
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
@@ -330,7 +340,7 @@ class MempoolBlocks {
this.rustGbtGenerator = new GbtGenerator();
}
private async $rustMakeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
public async $rustMakeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now();
// reset mempool short ids
@@ -346,16 +356,25 @@ class MempoolBlocks {
tx.inputs = tx.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[];
}
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]);
const convertedAccelerations = acceleratedList.map(acc => {
return {
uid: this.getUid(newMempool[acc.txid]),
delta: acc.feeDelta,
};
});
// run the block construction algorithm in a separate thread, and wait for a result
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
try {
const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], this.nextUid),
await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
);
if (saveResults) {
this.rustInitialized = true;
}
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, saveResults);
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, saveResults);
logger.debug(`RUST makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
return processed;
} catch (e) {
@@ -367,20 +386,20 @@ class MempoolBlocks {
return this.mempoolBlocks;
}
public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }): Promise<MempoolBlockWithTransactions[]> {
return this.$rustMakeBlockTemplates(newMempool, false);
public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
return this.$rustMakeBlockTemplates(newMempool, false, useAccelerations, accelerationPool);
}
public async $rustUpdateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[]): Promise<void> {
public async $rustUpdateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
// GBT optimization requires that uids never get too sparse
// as a sanity check, we should also explicitly prevent uint32 uid overflow
if (this.nextUid + added.length >= Math.min(Math.max(262144, 2 * mempoolSize), MAX_UINT32)) {
this.resetRustGbt();
}
if (!this.rustInitialized) {
// need to reset the worker
await this.$rustMakeBlockTemplates(newMempool, true);
return;
return this.$rustMakeBlockTemplates(newMempool, true, useAccelerations, accelerationPool);
}
const start = Date.now();
@@ -394,12 +413,22 @@ class MempoolBlocks {
}
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]);
const convertedAccelerations = acceleratedList.map(acc => {
return {
uid: this.getUid(newMempool[acc.txid]),
delta: acc.feeDelta,
};
});
// run the block construction algorithm in a separate thread, and wait for a result
try {
const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
await this.rustGbtGenerator.update(
added as RustThreadTransaction[],
removedUids,
convertedAccelerations as RustThreadAcceleration[],
this.nextUid,
),
);
@@ -407,19 +436,22 @@ class MempoolBlocks {
if (mempoolSize !== resultMempoolSize) {
throw new Error('GBT returned wrong number of transactions, cache is probably out of sync');
} else {
this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, true);
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, true);
this.removeUids(removedUids);
logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
return processed;
}
this.removeUids(removedUids);
logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
} catch (e) {
logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
this.resetRustGbt();
return this.mempoolBlocks;
}
}
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], saveResults): MempoolBlockWithTransactions[] {
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
for (const [txid, rate] of rates) {
if (txid in mempool) {
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
mempool[txid].effectiveFeePerVsize = rate;
mempool[txid].cpfpChecked = false;
}
@@ -463,11 +495,16 @@ class MempoolBlocks {
}
}
});
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
}
}
}
const isAccelerated : { [txid: string]: boolean } = {};
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended;
@@ -496,6 +533,26 @@ class MempoolBlocks {
mempoolTx.cpfpChecked = true;
}
const acceleration = accelerations[txid];
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
isAccelerated[ancestor.txid] = true;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
delete mempoolTx.acceleration;
}
// online calculation of stack-of-blocks fee stats
if (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) {
feeStatsCalculator.processNext(mempoolTx);
@@ -532,7 +589,7 @@ class MempoolBlocks {
private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
if (!feeStats) {
feeStats = Common.calcEffectiveFeeStatistics(transactions);
feeStats = Common.calcEffectiveFeeStatistics(transactions.filter(tx => !tx.acceleration));
}
return {
blockSize: totalSize,

View File

@@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import accelerationApi, { Acceleration } from './services/acceleration';
import redisCache from './redis-cache';
class Mempool {
@@ -19,9 +20,11 @@ class Mempool {
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[]) => void) | undefined;
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined;
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {};
private txPerSecondArray: number[] = [];
private txPerSecond: number = 0;
@@ -66,12 +69,12 @@ class Mempool {
}
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn;
}
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn;
}
@@ -107,10 +110,10 @@ class Mempool {
logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
}
if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []);
this.mempoolChangedCallback(this.mempoolCache, [], [], []);
}
if (this.$asyncMempoolChangedCallback) {
await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], []);
await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], [], []);
}
this.addToSpendMap(Object.values(this.mempoolCache));
}
@@ -123,7 +126,7 @@ class Mempool {
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) {
try {
const result = await bitcoinApi.$getMempoolTransactions(last_txid);
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
if (result) {
for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
@@ -231,31 +234,37 @@ class Mempool {
}
if (!loaded) {
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
this.updateTimerProgress(timer, 'fetched new transaction');
this.mempoolCache[txid] = transaction;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
newTransactions.push(transaction);
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
const sliceLength = 10000;
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);
logger.debug(`fetched ${txs.length} transactions`);
this.updateTimerProgress(timer, 'fetched new transactions');
if (config.REDIS.ENABLED) {
await redisCache.$addTransaction(transaction);
}
} catch (e: any) {
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
this.missingTxCount++;
}
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
for (const transaction of txs) {
this.mempoolCache[transaction.txid] = transaction;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
newTransactions.push(transaction);
if (config.REDIS.ENABLED) {
await redisCache.$addTransaction(transaction);
}
}
if (txs.length < slice.length) {
const missing = slice.length - txs.length;
if (config.MEMPOOL.BACKEND === 'esplora') {
this.missingTxCount += missing;
}
logger.debug(`Error finding ${missing} transactions in the mempool: `);
}
if (Date.now() - intervalTimer > Math.max(pollRate * 2, 5_000)) {
@@ -321,14 +330,19 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
const accelerationDelta = await this.$updateAccelerations();
if (accelerationDelta.length) {
hasChange = true;
}
this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
}
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions);
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta);
this.updateTimerProgress(timer, 'completed async mempool callback');
}
@@ -352,6 +366,70 @@ class Mempool {
this.clearTimer(timer);
}
public getAccelerations(): { [txid: string]: Acceleration } {
return this.accelerations;
}
public async $updateAccelerations(): Promise<string[]> {
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
return [];
}
try {
const newAccelerations = await accelerationApi.$fetchAccelerations();
const changed: string[] = [];
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
this.accelerations[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
this.accelerations = newAccelerationMap;
return changed;
} catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return [];
}
}
private startTimer() {
const state: any = {
start: Date.now(),

View File

@@ -12,6 +12,7 @@ import PricesRepository from '../../repositories/PricesRepository';
class MiningRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools', this.$listPools)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', this.$getPools)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', this.$getPoolHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', this.$getPoolBlocks)
@@ -41,6 +42,10 @@ class MiningRoutes {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Prices are not available on testnets.');
return;
}
if (req.query.timestamp) {
res.status(200).send(await PricesRepository.$getNearestHistoricalPrice(
parseInt(<string>req.query.timestamp ?? 0, 10)
@@ -88,6 +93,29 @@ class MiningRoutes {
}
}
private async $listPools(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
const pools = await mining.$listPools();
if (!pools) {
res.status(500).end();
return;
}
res.header('X-total-count', pools.length.toString());
if (pools.length === 0) {
res.status(204).send();
} else {
res.json(pools);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getPools(req: Request, res: Response) {
try {
const stats = await mining.$getPoolsStats(req.params.interval);

View File

@@ -26,7 +26,7 @@ class Mining {
/**
* Get historical blocks health
*/
public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
return await BlocksAuditsRepository.$getBlocksHealthHistory(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
@@ -56,7 +56,7 @@ class Mining {
/**
* Get historical block fee rates percentiles
*/
public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> {
public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockFeeRates(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
@@ -66,7 +66,7 @@ class Mining {
/**
* Get historical block sizes
*/
public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> {
public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockSizes(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
@@ -76,7 +76,7 @@ class Mining {
/**
* Get historical block weights
*/
public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> {
public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockWeights(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
@@ -107,6 +107,7 @@ class Mining {
slug: poolInfo.slug,
avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
avgFeeDelta: poolInfo.avgFeeDelta,
poolUniqueId: poolInfo.poolUniqueId
};
poolsStats.push(poolStat);
});
@@ -438,6 +439,7 @@ class Mining {
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
currentBits = oldestConsecutiveBlock.bits;
currentDifficulty = oldestConsecutiveBlock.difficulty;
}
let totalBlockChecked = 0;
@@ -593,6 +595,20 @@ class Mining {
}
}
/**
* List existing mining pools
*/
public async $listPools(): Promise<{name: string, slug: string, unique_id: number}[] | null> {
const [rows] = await database.query(`
SELECT
name,
slug,
unique_id
FROM pools`
);
return rows as {name: string, slug: string, unique_id: number}[];
}
private getDateMidnight(date: Date): Date {
date.setUTCHours(0);
date.setUTCMinutes(0);

View File

@@ -0,0 +1,19 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import pricesUpdater from '../../tasks/price-updater';
class PricesRoutes {
public initRoutes(app: Application): void {
app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this));
}
private $getCurrentPrices(req: Request, res: Response): void {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
res.json(pricesUpdater.getLatestPrices());
}
}
export default new PricesRoutes();

View File

@@ -0,0 +1,30 @@
import { query } from '../../utils/axios-query';
import config from '../../config';
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
export interface Acceleration {
txid: string,
feeDelta: number,
pools: number[],
}
class AccelerationApi {
public async $fetchAccelerations(): Promise<Acceleration[]> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
const response = await query(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`);
return (response as Acceleration[]) || [];
} else {
return [];
}
}
public isAcceleratedBlock(block: BlockExtended, accelerations: Acceleration[]): boolean {
let anyAccelerated = false;
for (let i = 0; i < accelerations.length && !anyAccelerated; i++) {
anyAccelerated = anyAccelerated || accelerations[i].pools?.includes(block.extras.pool.id);
}
return anyAccelerated;
}
}
export default new AccelerationApi();

View File

@@ -171,6 +171,7 @@ class StatisticsApi {
private getQueryForDaysAvg(div: number, interval: string) {
return `SELECT
UNIX_TIMESTAMP(added) as added,
CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions,
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
CAST(avg(vsize_1) as DOUBLE) as vsize_1,
CAST(avg(vsize_2) as DOUBLE) as vsize_2,
@@ -219,6 +220,7 @@ class StatisticsApi {
private getQueryForDays(div: number, interval: string) {
return `SELECT
UNIX_TIMESTAMP(added) as added,
CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions,
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
vsize_1,
vsize_2,
@@ -401,6 +403,7 @@ class StatisticsApi {
return statistic.map((s) => {
return {
added: s.added,
count: s.unconfirmed_transactions,
vbytes_per_second: s.vbytes_per_second,
mempool_byte_weight: s.mempool_byte_weight,
total_fee: s.total_fee,

View File

@@ -4,6 +4,8 @@ import { Common } from './common';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib';
import logger from '../logger';
import config from '../config';
import pLimit from '../utils/p-limit';
class TransactionUtils {
constructor() { }
@@ -71,6 +73,28 @@ class TransactionUtils {
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
}
public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> {
if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') {
const limiter = pLimit(8); // Run 8 requests at a time
const results = await Promise.allSettled(txids.map(
txid => limiter(() => this.$getMempoolTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore))
));
return results.filter(reply => reply.status === 'fulfilled')
.map(r => (r as PromiseFulfilledResult<MempoolTransactionExtended>).value);
} else {
const transactions = await bitcoinApi.$getMempoolTransactions(txids);
return transactions.map(transaction => {
if (Common.isLiquid()) {
if (!isFinite(Number(transaction.fee))) {
transaction.fee = Object.values(transaction.fee || {}).reduce((total, output) => total + output, 0);
}
}
return this.extendMempoolTransaction(transaction);
});
}
}
public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
// @ts-ignore
if (transaction.vsize) {

View File

@@ -21,6 +21,8 @@ import Audit from './audit';
import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
// valid 'want' subscriptions
const wantable = [
@@ -172,9 +174,15 @@ class WebsocketHandler {
}
const tx = memPool.getMempool()[trackTxid];
if (tx && tx.position) {
const position: { block: number, vsize: number, accelerated?: boolean } = {
...tx.position
};
if (tx.acceleration) {
position.accelerated = tx.acceleration;
}
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position: tx.position,
position
});
}
} else {
@@ -190,18 +198,14 @@ class WebsocketHandler {
matchedAddress = matchedAddress.toLowerCase();
}
if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
client['track-address'] = null;
client['track-scriptpubkey'] = '41' + matchedAddress + 'ac';
} else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
client['track-address'] = null;
client['track-scriptpubkey'] = '21' + matchedAddress + 'ac';
client['track-address'] = '41' + matchedAddress + 'ac';
} else if (/^(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
client['track-address'] = '21' + matchedAddress + 'ac';
} else {
client['track-address'] = matchedAddress;
client['track-scriptpubkey'] = null;
}
} else {
client['track-address'] = null;
client['track-scriptpubkey'] = null;
}
}
@@ -390,7 +394,7 @@ class WebsocketHandler {
}
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]): Promise<void> {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
}
@@ -399,9 +403,9 @@ class WebsocketHandler {
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
if (config.MEMPOOL.RUST_GBT) {
await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions);
await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions, config.MEMPOOL_SERVICES.ACCELERATIONS);
} else {
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
}
} else {
mempoolBlocks.updateMempoolBlocks(newMempool, true);
@@ -480,6 +484,10 @@ class WebsocketHandler {
}
}
// pre-compute address transactions
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
this.wss.clients.forEach(async (client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
@@ -519,78 +527,17 @@ class WebsocketHandler {
}
if (client['track-address']) {
const foundTransactions: TransactionExtended[] = [];
const newTransactions = Array.from(addressCache[client['track-address']]?.values() || []);
const removedTransactions = Array.from(removedAddressCache[client['track-address']]?.values() || []);
// txs may be missing prevouts in non-esplora backends
// so fetch the full transactions now
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(newTransactions) : newTransactions;
for (const tx of newTransactions) {
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address']);
if (someVin) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
} else {
foundTransactions.push(tx);
}
return;
}
const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']);
if (someVout) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
} else {
foundTransactions.push(tx);
}
}
if (removedTransactions.length) {
response['address-removed-transactions'] = JSON.stringify(removedTransactions);
}
if (foundTransactions.length) {
response['address-transactions'] = JSON.stringify(foundTransactions);
}
}
if (client['track-scriptpubkey']) {
const foundTransactions: TransactionExtended[] = [];
for (const tx of newTransactions) {
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']);
if (someVin) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
} else {
foundTransactions.push(tx);
}
return;
}
const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']);
if (someVout) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
} else {
foundTransactions.push(tx);
}
}
}
if (foundTransactions.length) {
response['address-transactions'] = JSON.stringify(foundTransactions);
if (fullTransactions.length) {
response['address-transactions'] = JSON.stringify(fullTransactions);
}
}
@@ -598,7 +545,6 @@ class WebsocketHandler {
const foundTransactions: TransactionExtended[] = [];
newTransactions.forEach((tx) => {
if (client['track-asset'] === Common.nativeAssetId) {
if (tx.vin.some((vin) => !!vin.is_pegin)) {
foundTransactions.push(tx);
@@ -645,10 +591,25 @@ class WebsocketHandler {
const mempoolTx = newMempool[trackTxid];
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = JSON.stringify({
const positionData = {
txid: trackTxid,
position: mempoolTx.position,
});
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
};
if (mempoolTx.cpfpDirty) {
positionData['cpfp'] = {
ancestors: mempoolTx.ancestors,
bestDescendant: mempoolTx.bestDescendant || null,
descendants: mempoolTx.descendants || null,
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
sigops: mempoolTx.sigops,
adjustedVsize: mempoolTx.adjustedVsize,
acceleration: mempoolTx.acceleration
};
}
response['txPosition'] = JSON.stringify(positionData);
}
}
@@ -695,6 +656,7 @@ class WebsocketHandler {
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
let projectedBlocks;
let auditMempool = _memPool;
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
// template calculation functions have mempool side effects, so calculate audits using
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
@@ -702,19 +664,27 @@ class WebsocketHandler {
auditMempool = deepClone(_memPool);
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
if (config.MEMPOOL.RUST_GBT) {
projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool);
projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool, isAccelerated, block.extras.pool.id);
} else {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
}
} else {
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
}
} else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
if (config.MEMPOOL.RUST_GBT) {
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(auditMempool, Object.keys(auditMempool).length, [], [], isAccelerated, block.extras.pool.id);
} else {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
}
} else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
}
}
if (Common.indexingEnabled()) {
const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const { censored, added, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@@ -743,6 +713,7 @@ class WebsocketHandler {
freshTxs: fresh,
sigopTxs: sigop,
fullrbfTxs: fullrbf,
acceleratedTxs: accelerated,
matchRate: matchRate,
expectedFees: totalFees,
expectedWeight: totalWeight,
@@ -770,9 +741,9 @@ class WebsocketHandler {
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
if (config.MEMPOOL.RUST_GBT) {
await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions);
await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions, true);
} else {
await mempoolBlocks.$makeBlockTemplates(_memPool, true);
await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
}
} else {
mempoolBlocks.updateMempoolBlocks(_memPool, true);
@@ -784,6 +755,9 @@ class WebsocketHandler {
const fees = feeApi.getRecommendedFee();
const mempoolInfo = memPool.getMempoolInfo();
// pre-compute address transactions
const addressCache = this.makeAddressCache(transactions);
// update init data
this.updateSocketDataFields({
'mempoolInfo': mempoolInfo,
@@ -836,51 +810,17 @@ class WebsocketHandler {
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position: mempoolTx.position,
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
});
}
}
}
if (client['track-address']) {
const foundTransactions: TransactionExtended[] = [];
transactions.forEach((tx) => {
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address'])) {
foundTransactions.push(tx);
return;
}
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address'])) {
foundTransactions.push(tx);
}
});
if (foundTransactions.length) {
foundTransactions.forEach((tx) => {
tx.status = {
confirmed: true,
block_height: block.height,
block_hash: block.id,
block_time: block.timestamp,
};
});
response['block-transactions'] = JSON.stringify(foundTransactions);
}
}
if (client['track-scriptpubkey']) {
const foundTransactions: TransactionExtended[] = [];
transactions.forEach((tx) => {
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) {
foundTransactions.push(tx);
return;
}
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) {
foundTransactions.push(tx);
}
});
const foundTransactions: TransactionExtended[] = Array.from(addressCache[client['track-address']]?.values() || []);
if (foundTransactions.length) {
foundTransactions.forEach((tx) => {
@@ -958,6 +898,52 @@ class WebsocketHandler {
+ '}';
}
private makeAddressCache(transactions: MempoolTransactionExtended[]): { [address: string]: Set<MempoolTransactionExtended> } {
const addressCache: { [address: string]: Set<MempoolTransactionExtended> } = {};
for (const tx of transactions) {
for (const vin of tx.vin) {
if (vin?.prevout?.scriptpubkey_address) {
if (!addressCache[vin.prevout.scriptpubkey_address]) {
addressCache[vin.prevout.scriptpubkey_address] = new Set();
}
addressCache[vin.prevout.scriptpubkey_address].add(tx);
}
if (vin?.prevout?.scriptpubkey) {
if (!addressCache[vin.prevout.scriptpubkey]) {
addressCache[vin.prevout.scriptpubkey] = new Set();
}
addressCache[vin.prevout.scriptpubkey].add(tx);
}
}
for (const vout of tx.vout) {
if (vout?.scriptpubkey_address) {
if (!addressCache[vout?.scriptpubkey_address]) {
addressCache[vout?.scriptpubkey_address] = new Set();
}
addressCache[vout?.scriptpubkey_address].add(tx);
}
if (vout?.scriptpubkey) {
if (!addressCache[vout.scriptpubkey]) {
addressCache[vout.scriptpubkey] = new Set();
}
addressCache[vout.scriptpubkey].add(tx);
}
}
}
return addressCache;
}
private async getFullTransactions(transactions: MempoolTransactionExtended[]): Promise<MempoolTransactionExtended[]> {
for (let i = 0; i < transactions.length; i++) {
try {
transactions[i] = await transactionUtils.$getMempoolTransactionExtended(transactions[i].txid, true);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
}
return transactions;
}
private printLogs(): void {
if (this.wss) {
const count = this.wss?.clients?.size || 0;

View File

@@ -38,11 +38,13 @@ interface IConfig {
DISK_CACHE_BLOCK_INTERVAL: number;
MAX_PUSH_TX_SIZE_WEIGHT: number;
ALLOW_UNREACHABLE: boolean;
PRICE_UPDATES_PER_HOUR: number;
};
ESPLORA: {
REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null;
RETRY_UNIX_SOCKET_AFTER: number;
FALLBACK: string[];
};
LIGHTNING: {
ENABLED: boolean;
@@ -115,10 +117,6 @@ interface IConfig {
USERNAME: string;
PASSWORD: string;
};
PRICE_DATA_SERVER: {
TOR_URL: string;
CLEARNET_URL: string;
};
EXTERNAL_DATA_SERVER: {
MEMPOOL_API: string;
MEMPOOL_ONION: string;
@@ -139,6 +137,10 @@ interface IConfig {
AUDIT_START_HEIGHT: number;
SERVERS: string[];
},
MEMPOOL_SERVICES: {
API: string;
ACCELERATIONS: boolean;
},
REDIS: {
ENABLED: boolean;
UNIX_SOCKET_PATH: string;
@@ -181,11 +183,13 @@ const defaults: IConfig = {
'DISK_CACHE_BLOCK_INTERVAL': 6,
'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
'ALLOW_UNREACHABLE': true,
'PRICE_UPDATES_PER_HOUR': 1,
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null,
'RETRY_UNIX_SOCKET_AFTER': 30000,
'FALLBACK': [],
},
'ELECTRUM': {
'HOST': '127.0.0.1',
@@ -258,10 +262,6 @@ const defaults: IConfig = {
'USERNAME': '',
'PASSWORD': ''
},
'PRICE_DATA_SERVER': {
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
},
'EXTERNAL_DATA_SERVER': {
'MEMPOOL_API': 'https://mempool.space/api/v1',
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
@@ -282,6 +282,10 @@ const defaults: IConfig = {
'AUDIT_START_HEIGHT': 774000,
'SERVERS': [],
},
'MEMPOOL_SERVICES': {
'API': '',
'ACCELERATIONS': false,
},
'REDIS': {
'ENABLED': false,
'UNIX_SOCKET_PATH': '',
@@ -302,10 +306,10 @@ class Config implements IConfig {
LND: IConfig['LND'];
CLIGHTNING: IConfig['CLIGHTNING'];
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
MAXMIND: IConfig['MAXMIND'];
REPLICATION: IConfig['REPLICATION'];
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS'];
constructor() {
@@ -323,10 +327,10 @@ class Config implements IConfig {
this.LND = configs.LND;
this.CLIGHTNING = configs.CLIGHTNING;
this.SOCKS5PROXY = configs.SOCKS5PROXY;
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
this.MAXMIND = configs.MAXMIND;
this.REPLICATION = configs.REPLICATION;
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS;
}

View File

@@ -30,6 +30,7 @@ import generalLightningRoutes from './api/explorer/general.routes';
import lightningStatsUpdater from './tasks/lightning/stats-updater.service';
import networkSyncService from './tasks/lightning/network-sync.service';
import statisticsRoutes from './api/statistics/statistics.routes';
import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes';
import bisqRoutes from './api/bisq/bisq.routes';
import liquidRoutes from './api/liquid/liquid.routes';
@@ -90,6 +91,10 @@ class Server {
async startServer(worker = false): Promise<void> {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
if (config.MEMPOOL.BACKEND === 'esplora') {
bitcoinApi.startHealthChecks();
}
if (config.DATABASE.ENABLED) {
await DB.checkDbConnection();
try {
@@ -193,6 +198,7 @@ class Server {
await memPool.$updateMempool(newMempool, pollRate);
}
indexer.$run();
priceUpdater.$run();
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
const elapsed = Date.now() - start;
@@ -261,6 +267,7 @@ class Server {
setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app);
pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app);
}

View File

@@ -105,6 +105,12 @@ class Indexer {
return;
}
try {
await priceUpdater.$run();
} catch (e) {
logger.err(`Running priceUpdater failed. Reason: ` + (e instanceof Error ? e.message : e));
}
// Do not attempt to index anything unless Bitcoin Core is fully synced
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks !== blockchainInfo.headers) {
@@ -119,8 +125,6 @@ class Indexer {
await this.checkAvailableCoreIndexes();
try {
await priceUpdater.$run();
const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) {
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration

View File

@@ -20,6 +20,7 @@ export interface PoolInfo {
slug: string;
avgMatchRate: number | null;
avgFeeDelta: number | null;
poolUniqueId: number;
}
export interface PoolStats extends PoolInfo {
@@ -36,6 +37,7 @@ export interface BlockAudit {
sigopTxs: string[],
fullrbfTxs: string[],
addedTxs: string[],
acceleratedTxs: string[],
matchRate: number,
expectedFees?: number,
expectedWeight?: number,
@@ -91,6 +93,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
block: number,
vsize: number,
};
acceleration?: boolean;
uid?: number;
}
@@ -101,6 +104,7 @@ export interface MempoolTransactionExtended extends TransactionExtended {
adjustedFeePerVsize: number;
inputs?: number[];
lastBoosted?: number;
cpfpDirty?: boolean;
}
export interface AuditTransaction {
@@ -182,6 +186,7 @@ export interface TransactionStripped {
fee: number;
vsize: number;
value: number;
acc?: boolean;
rate?: number; // effective fee rate
}

View File

@@ -116,6 +116,7 @@ class AuditReplication {
freshTxs: auditSummary.freshTxs || [],
sigopTxs: auditSummary.sigopTxs || [],
fullrbfTxs: auditSummary.fullrbfTxs || [],
acceleratedTxs: auditSummary.acceleratedTxs || [],
matchRate: auditSummary.matchRate,
expectedFees: auditSummary.expectedFees,
expectedWeight: auditSummary.expectedWeight,

View File

@@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> {
try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, match_rate, expected_fees, expected_weight)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
@@ -69,6 +69,7 @@ class BlocksAuditRepositories {
fresh_txs as freshTxs,
sigop_txs as sigopTxs,
fullrbf_txs as fullrbfTxs,
accelerated_txs as acceleratedTxs,
match_rate as matchRate,
expected_fees as expectedFees,
expected_weight as expectedWeight
@@ -83,6 +84,7 @@ class BlocksAuditRepositories {
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
rows[0].template = JSON.parse(rows[0].template);
return rows[0];

View File

@@ -40,7 +40,8 @@ class PoolsRepository {
pools.link AS link,
slug,
AVG(blocks_audits.match_rate) AS avgMatchRate,
AVG((CAST(blocks.fees as SIGNED) - CAST(blocks_audits.expected_fees as SIGNED)) / NULLIF(CAST(blocks_audits.expected_fees as SIGNED), 0)) AS avgFeeDelta
AVG((CAST(blocks.fees as SIGNED) - CAST(blocks_audits.expected_fees as SIGNED)) / NULLIF(CAST(blocks_audits.expected_fees as SIGNED), 0)) AS avgFeeDelta,
unique_id as poolUniqueId
FROM blocks
JOIN pools on pools.id = pool_id
LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height

View File

@@ -25,7 +25,10 @@ export interface PriceHistory {
class PriceUpdater {
public historyInserted = false;
private lastRun = 0;
private timeBetweenUpdatesMs = 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR;
private cyclePosition = -1;
private firstRun = true;
private lastTime = -1;
private lastHistoricalRun = 0;
private running = false;
private feeds: PriceFeed[] = [];
@@ -41,6 +44,8 @@ class PriceUpdater {
this.feeds.push(new CoinbaseApi());
this.feeds.push(new BitfinexApi());
this.feeds.push(new GeminiApi());
this.setCyclePosition();
}
public getLatestPrices(): ApiPrice {
@@ -100,22 +105,48 @@ class PriceUpdater {
this.running = false;
}
private getMillisecondsSinceBeginningOfHour(): number {
const now = new Date();
const beginningOfHour = new Date(now);
beginningOfHour.setMinutes(0, 0, 0);
return now.getTime() - beginningOfHour.getTime();
}
private setCyclePosition(): void {
const millisecondsSinceBeginningOfHour = this.getMillisecondsSinceBeginningOfHour();
for (let i = 0; i < config.MEMPOOL.PRICE_UPDATES_PER_HOUR; i++) {
if (this.timeBetweenUpdatesMs * i > millisecondsSinceBeginningOfHour) {
this.cyclePosition = i;
return;
}
}
this.cyclePosition = config.MEMPOOL.PRICE_UPDATES_PER_HOUR;
}
/**
* Fetch last BTC price from exchanges, average them, and save it in the database once every hour
*/
private async $updatePrice(): Promise<void> {
if (this.lastRun === 0 && config.DATABASE.ENABLED === true) {
this.lastRun = await PricesRepository.$getLatestPriceTime();
let forceUpdate = false;
if (this.firstRun === true && config.DATABASE.ENABLED === true) {
const lastUpdate = await PricesRepository.$getLatestPriceTime();
if (new Date().getTime() / 1000 - lastUpdate > this.timeBetweenUpdatesMs / 1000) {
forceUpdate = true;
}
this.firstRun = false;
}
if ((Math.round(new Date().getTime() / 1000) - this.lastRun) < 3600) {
// Refresh only once every hour
const millisecondsSinceBeginningOfHour = this.getMillisecondsSinceBeginningOfHour();
// Reset the cycle on new hour
if (this.lastTime > millisecondsSinceBeginningOfHour) {
this.cyclePosition = 0;
}
this.lastTime = millisecondsSinceBeginningOfHour;
if (millisecondsSinceBeginningOfHour < this.timeBetweenUpdatesMs * this.cyclePosition && !forceUpdate && this.cyclePosition !== 0) {
return;
}
const previousRun = this.lastRun;
this.lastRun = new Date().getTime() / 1000;
for (const currency of this.currencies) {
let prices: number[] = [];
@@ -146,26 +177,27 @@ class PriceUpdater {
}
}
logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`);
if (config.DATABASE.ENABLED === true) {
if (config.DATABASE.ENABLED === true && this.cyclePosition === 0) {
// Save everything in db
try {
const p = 60 * 60 * 1000; // milliseconds in an hour
const nowRounded = new Date(Math.round(new Date().getTime() / p) * p); // https://stackoverflow.com/a/28037042
this.latestPrices.time = nowRounded.getTime() / 1000;
await PricesRepository.$savePrices(nowRounded.getTime() / 1000, this.latestPrices);
} catch (e) {
this.lastRun = previousRun + 5 * 60;
logger.err(`Cannot save latest prices into db. Trying again in 5 minutes. Reason: ${(e instanceof Error ? e.message : e)}`);
}
}
this.latestPrices.time = Math.round(new Date().getTime() / 1000);
logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`);
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.latestPrices);
}
this.lastRun = new Date().getTime() / 1000;
if (!forceUpdate) {
this.cyclePosition++;
}
if (this.latestPrices.USD === -1) {
this.latestPrices = await PricesRepository.$getLatestConversionRates();

View File

@@ -0,0 +1,179 @@
/*
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
How it works:
`this._head` is an instance of `Node` which keeps track of its current value and nests
another instance of `Node` that keeps the value that comes after it. When a value is
provided to `.enqueue()`, the code needs to iterate through `this._head`, going deeper
and deeper to find the last value. However, iterating through every single item is slow.
This problem is solved by saving a reference to the last value as `this._tail` so that
it can reference it to add a new value.
*/
class Node {
value;
next;
constructor(value) {
this.value = value;
}
}
class Queue {
private _head;
private _tail;
private _size;
constructor() {
this.clear();
}
enqueue(value) {
const node = new Node(value);
if (this._head) {
this._tail.next = node;
this._tail = node;
} else {
this._head = node;
this._tail = node;
}
this._size++;
}
dequeue() {
const current = this._head;
if (!current) {
return;
}
this._head = this._head.next;
this._size--;
return current.value;
}
clear() {
this._head = undefined;
this._tail = undefined;
this._size = 0;
}
get size() {
return this._size;
}
*[Symbol.iterator]() {
let current = this._head;
while (current) {
yield current.value;
current = current.next;
}
}
}
interface LimitFunction {
readonly activeCount: number;
readonly pendingCount: number;
clearQueue: () => void;
<Arguments extends unknown[], ReturnType>(
fn: (...args: Arguments) => PromiseLike<ReturnType> | ReturnType,
...args: Arguments
): Promise<ReturnType>;
}
export default function pLimit(concurrency: number): LimitFunction {
if (
!(
(Number.isInteger(concurrency) ||
concurrency === Number.POSITIVE_INFINITY) &&
concurrency > 0
)
) {
throw new TypeError('Expected `concurrency` to be a number from 1 and up');
}
const queue = new Queue();
let activeCount = 0;
const next = () => {
activeCount--;
if (queue.size > 0) {
queue.dequeue()();
}
};
const run = async (fn, resolve, args) => {
activeCount++;
const result = (async () => fn(...args))();
resolve(result);
try {
await result;
} catch {}
next();
};
const enqueue = (fn, resolve, args) => {
queue.enqueue(run.bind(undefined, fn, resolve, args));
(async () => {
// This function needs to wait until the next microtask before comparing
// `activeCount` to `concurrency`, because `activeCount` is updated asynchronously
// when the run function is dequeued and called. The comparison in the if-statement
// needs to happen asynchronously as well to get an up-to-date value for `activeCount`.
await Promise.resolve();
if (activeCount < concurrency && queue.size > 0) {
queue.dequeue()();
}
})();
};
const generator = (fn, ...args) =>
new Promise((resolve) => {
enqueue(fn, resolve, args);
});
Object.defineProperties(generator, {
activeCount: {
get: () => activeCount,
},
pendingCount: {
get: () => queue.size,
},
clearQueue: {
value: () => {
queue.clear();
},
},
});
return generator as any;
}

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file with sha256 hash c80c5ee4c71c5a76a1f6cd35339bd0c45b25b491933ea7b02a66470e9f43a6fd.
Signed: TheBlueMatt

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of August 2, 2023.
Signed: andrewtoth

View File

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

View File

@@ -113,7 +113,8 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6
"DISK_CACHE_BLOCK_INTERVAL": 6,
"PRICE_UPDATES_PER_HOUR": 1
},
```
@@ -146,6 +147,7 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
MEMPOOL_PRICE_UPDATES_PER_HOUR: ""
...
```
@@ -363,25 +365,6 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```json
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
}
```
Corresponding `docker-compose.yml` overrides:
```yaml
api:
environment:
PRICE_DATA_SERVER_TOR_URL: ""
PRICE_DATA_SERVER_CLEARNET_URL: ""
...
```
<br/>
`mempool-config.json`:
```json
"LIGHTNING": {

View File

@@ -33,7 +33,8 @@
"MAX_PUSH_TX_SIZE_WEIGHT": __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__,
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
@@ -50,7 +51,8 @@
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"FALLBACK": __ESPLORA_FALLBACK__
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
@@ -111,10 +113,6 @@
"USERNAME": "__SOCKS5PROXY_USERNAME__",
"PASSWORD": "__SOCKS5PROXY_PASSWORD__"
},
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
@@ -135,6 +133,10 @@
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
"SERVERS": __REPLICATION_SERVERS__
},
"MEMPOOL_SERVICES": {
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"

View File

@@ -35,7 +35,7 @@ __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000}
__MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true}
__MEMPOOL_PRICE_UPDATES_PER_HOUR__=${MEMPOOL_PRICE_UPDATES_PER_HOUR:=1}
# CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@@ -53,6 +53,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
# SECOND_CORE_RPC
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
@@ -94,10 +95,6 @@ __SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050}
__SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""}
__SOCKS5PROXY_PASSWORD__=${SOCKS5PROXY_PASSWORD:=""}
# PRICE_DATA_SERVER
__PRICE_DATA_SERVER_TOR_URL__=${PRICE_DATA_SERVER_TOR_URL:=http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices}
__PRICE_DATA_SERVER_CLEARNET_URL__=${PRICE_DATA_SERVER_CLEARNET_URL:=https://price.bisq.wiz.biz/getAllMarketPrices}
# EXTERNAL_DATA_SERVER
__EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https://mempool.space/api/v1}
__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1}
@@ -137,6 +134,10 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
@@ -177,6 +178,7 @@ sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_PRICE_UPDATES_PER_HOUR__!${__MEMPOOL_PRICE_UPDATES_PER_HOUR__}!g" mempool-config.json
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
@@ -191,6 +193,7 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json
@@ -226,9 +229,6 @@ sed -i "s!__SOCKS5PROXY_PORT__!${__SOCKS5PROXY_PORT__}!g" mempool-config.json
sed -i "s!__SOCKS5PROXY_USERNAME__!${__SOCKS5PROXY_USERNAME__}!g" mempool-config.json
sed -i "s!__SOCKS5PROXY_PASSWORD__!${__SOCKS5PROXY_PASSWORD__}!g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_API__!${__EXTERNAL_DATA_SERVER_MEMPOOL_API__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__!${__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_API__!${__EXTERNAL_DATA_SERVER_LIQUID_API__}!g" mempool-config.json
@@ -267,6 +267,10 @@ sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
# MEMPOOL_SERVICES
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
# REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json

View File

@@ -42,9 +42,6 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
'use strict'
import 'cypress-wait-until';
import { PageIdleDetector } from './PageIdleDetector';
import { mockWebSocket } from './websocket';

View File

@@ -14,6 +14,7 @@
// ***********************************************************
// When a command from ./commands is ready to use, import with `import './commands'` syntax
import 'cypress-wait-until';
import './commands';
import failOnConsoleError from 'cypress-fail-on-console-error';

View File

@@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"include": ["**/*.ts"],
"compilerOptions": {
"types": ["cypress"],
"types": ["cypress", "node", "cypress-wait-until"],
"lib": ["es2015", "dom"],
"allowJs": true,
"noEmit": true,

View File

@@ -22,5 +22,6 @@
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
"LIGHTNING": false,
"HISTORICAL_PRICE": true
"HISTORICAL_PRICE": true,
"ACCELERATOR": false
}

File diff suppressed because it is too large Load Diff

View File

@@ -61,18 +61,18 @@
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^16.1.4",
"@angular/animations": "^16.1.5",
"@angular/cli": "^16.1.4",
"@angular/common": "^16.1.5",
"@angular/compiler": "^16.1.5",
"@angular/core": "^16.1.5",
"@angular/forms": "^16.1.5",
"@angular/localize": "^16.1.5",
"@angular/platform-browser": "^16.1.5",
"@angular/platform-browser-dynamic": "^16.1.5",
"@angular/platform-server": "^16.1.5",
"@angular/router": "^16.1.5",
"@angular-devkit/build-angular": "^16.2.0",
"@angular/animations": "^16.2.2",
"@angular/cli": "^16.2.0",
"@angular/common": "^16.2.2",
"@angular/compiler": "^16.2.2",
"@angular/core": "^16.2.2",
"@angular/forms": "^16.2.2",
"@angular/localize": "^16.2.2",
"@angular/platform-browser": "^16.2.2",
"@angular/platform-browser-dynamic": "^16.2.2",
"@angular/platform-server": "^16.2.2",
"@angular/router": "^16.2.2",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-common-types": "~6.4.0",
"@fortawesome/fontawesome-svg-core": "~6.4.0",
@@ -110,9 +110,10 @@
},
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"cypress": "^12.17.1",
"@types/cypress": "^1.1.3",
"cypress": "^12.17.2",
"cypress-fail-on-console-error": "~4.0.3",
"cypress-wait-until": "^1.7.2",
"cypress-wait-until": "^2.0.0",
"mock-socket": "~9.2.1",
"start-server-and-test": "~2.0.0"
},

View File

@@ -112,6 +112,14 @@ PROXY_CONFIG.push(...[
"^/testnet": ""
},
},
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://127.0.0.1:8999`,

View File

@@ -112,6 +112,14 @@ PROXY_CONFIG.push(...[
"^/testnet": ""
},
},
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,

View File

@@ -95,6 +95,14 @@ if (configContent && configContent.BASE_MODULE === 'bisq') {
}
PROXY_CONFIG.push(...[
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,

View File

@@ -4,6 +4,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
import { StartComponent } from './components/start/start.component';
import { TransactionComponent } from './components/transaction/transaction.component';
import { BlockComponent } from './components/block/block.component';
import { BlockViewComponent } from './components/block-view/block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
@@ -373,6 +374,10 @@ let routes: Routes = [
path: 'clock/:mode/:index',
component: ClockComponent,
},
{
path: 'view/block/:id',
component: BlockViewComponent,
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },

View File

@@ -41,12 +41,14 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`);
return this.bisqApiService.getAddress$(this.addressString)
.pipe(
catchError((err) => {
this.isLoadingAddress = false;
this.error = err;
this.seoService.logSoft404();
console.log(err);
return of(null);
})
@@ -62,6 +64,7 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
(error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingAddress = false;
});
}

View File

@@ -69,6 +69,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
this.location.replaceState(
this.router.createUrlTree(['/bisq/block/', hash]).toString()
);
this.seoService.updateCanonical(this.location.path());
return this.bisqApiService.getBlock$(this.blockHash)
.pipe(catchError(this.caughtHttpError.bind(this)));
}),
@@ -82,11 +83,13 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
)
.subscribe((block: BisqBlock) => {
if (!block) {
this.seoService.logSoft404();
return;
}
this.isLoading = false;
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`);
this.block = block;
});
}
@@ -97,6 +100,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
caughtHttpError(err: HttpErrorResponse){
this.error = err;
this.seoService.logSoft404();
return of(null);
}
}

View File

@@ -36,6 +36,7 @@ export class BisqBlocksComponent implements OnInit {
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 670) {

View File

@@ -29,7 +29,8 @@ export class BisqDashboardComponent implements OnInit {
) { }
ngOnInit(): void {
this.seoService.setTitle(`Markets`);
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more.`);
this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()

View File

@@ -34,6 +34,7 @@ export class BisqMainDashboardComponent implements OnInit {
ngOnInit(): void {
this.seoService.resetTitle();
this.seoService.resetDescription();
this.websocketService.want(['blocks']);
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(

View File

@@ -48,7 +48,8 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
map(([markets, routeParams]) => {
const pair = routeParams.get('pair');
const pairUpperCase = pair.replace('_', '/').toUpperCase();
this.seoService.setTitle(`Bisq market: ${pairUpperCase}`);
this.seoService.setTitle($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`);
this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`);
return {
pair: pairUpperCase,

View File

@@ -26,6 +26,7 @@ export class BisqStatsComponent implements OnInit {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
this.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`);
this.stateService.bsqPrice$
.subscribe((bsqPrice) => {
this.price = bsqPrice;

View File

@@ -48,6 +48,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
document.body.scrollTo(0, 0);
this.txId = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transaction:See inputs, outputs, transaction type, burnt amount, and more for transaction with txid ${this.txId}:INTERPOLATION:.`);
if (history.state.data) {
return of(history.state.data);
}
@@ -70,11 +71,13 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
catchError((txError: HttpErrorResponse) => {
console.log(txError);
this.error = txError;
this.seoService.logSoft404();
return of(null);
})
);
}
this.error = bisqTxError;
this.seoService.logSoft404();
return of(null);
})
);
@@ -103,6 +106,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
this.isLoadingTx = false;
if (!tx) {
this.seoService.logSoft404();
return;
}

View File

@@ -79,6 +79,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transactions:See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more.`);
this.radioGroupForm = this.formBuilder.group({
txTypes: [this.txTypesDefaultChecked],

View File

@@ -4,12 +4,13 @@
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&reg;</span>
<img class="logo" src="/resources/mempool-logo-bigger.png" />
<div class="version">
v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
<span>v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]</span>
<span *ngIf="stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE">[{{ stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE }}]</span>
</div>
</div>
<div class="about-text">
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &trade;</ng-template></h5>
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &reg;</ng-template></h5>
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
</div>
@@ -31,6 +32,14 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<ng-container *ngIf="false && officialMempoolSpace">
<h3 class="mt-5">Sponsor the project</h3>
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
</div>
</ng-container>
<div class="enterprise-sponsor" id="enterprise-sponsors">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
<div class="wrapper">
@@ -173,34 +182,44 @@
</svg>
<span>Exodus</span>
</a>
<a href="https://www.luminex.io" target="_blank" title="Luminex">
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="66.95" height="80" viewBox="0 0 300.43 385" style="padding-top: 10px;">
<defs>
<style>
.lum-cls-1 {
fill: #f2ea25;
}
</style>
</defs>
<path class="lum-cls-1" d="m309.02,90.04c0,49.65-38.73,90.04-95.34,90.04s-95.34-40.39-95.34-90.04S153.77,0,213.69,0c56.28,0,95.34,40.39,95.34,90.04Zm-63.56,0c0-20.52-14.23-37.07-31.78-37.07s-31.78,16.55-31.78,37.07,14.23,37.07,31.78,37.07,31.78-16.55,31.78-37.07Z"/>
<path class="lum-cls-1" d="m311.87,372.67h-66.34l-31.84-47.76-31.84,47.76h-66.34l58.38-90.22-53.07-79.61h66.34l26.54,42.46,26.53-42.46h66.34l-53.07,79.61,58.38,90.22Z"/>
<rect class="lum-cls-1" width="60.69" height="372.67"/>
</svg>
<span>Luminex</span>
</a>
</div>
</div>
<div class="community-sponsor" id="community-sponsors">
<h3 i18n="about.sponsors.withHeart">Community Sponsors ❤️</h3>
<ng-container *ngIf="officialMempoolSpace">
<div *ngIf="profiles$ | async as profiles" id="community-sponsors">
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper">
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
</div>
</div>
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.chads.length > 0">
<h3 i18n="about.sponsors.withHeart">Chad Sponsors</h3>
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
</div>
</div>
</ng-container>
<div class="community-sponsor" style="margin-bottom: 68px">
<h3 i18n="about.sponsors.withHeart">OG Sponsors ❤️</h3>
<div class="wrapper">
<ng-container *ngIf="sponsors$ | async as sponsors; else loadingSponsors">
<ng-template ngFor let-sponsor [ngForOf]="sponsors">
<a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored" [title]="sponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + sponsor.handle" />
</a>
</ng-template>
<ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
<a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-container>
</div>
</div>
@@ -224,7 +243,7 @@
<img class="image" src="/resources/profile/ronindojo.png" />
<span>RoninDojo</span>
</a>
<a href="https://github.com/runcitadel/core" target="_blank" title="Citadel">
<a href="https://github.com/runcitadel" target="_blank" title="Citadel">
<img class="image" src="/resources/profile/runcitadel.svg" />
<span>Citadel</span>
</a>
@@ -340,7 +359,7 @@
<div class="wrapper">
<ng-template ngFor let-translator [ngForOf]="translators">
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" />
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -354,7 +373,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.regular">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" />
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>
@@ -366,7 +385,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.core">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" />
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>

View File

@@ -10,6 +10,9 @@
margin: 25px;
line-height: 32px;
}
.unknown {
border: 1px solid #b4b4b4;
}
.image.not-rounded {
border-radius: 0;
@@ -19,6 +22,7 @@
.intro {
margin: 25px auto 30px;
margin-top: 25px;
width: 250px;
display: flex;
flex-direction: column;

View File

@@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { IBackendInfo } from '../../interfaces/websocket.interface';
import { Router, ActivatedRoute } from '@angular/router';
import { map, tap } from 'rxjs/operators';
import { map, share, tap } from 'rxjs/operators';
import { ITranslators } from '../../interfaces/node-api.interface';
import { DOCUMENT } from '@angular/common';
@@ -19,14 +19,16 @@ import { DOCUMENT } from '@angular/common';
export class AboutComponent implements OnInit {
@ViewChild('promoVideo') promoVideo: ElementRef;
backendInfo$: Observable<IBackendInfo>;
sponsors$: Observable<any>;
translators$: Observable<ITranslators>;
allContributors$: Observable<any>;
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;
packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
showNavigateToSponsor = false;
profiles$: Observable<any>;
translators$: Observable<ITranslators>;
allContributors$: Observable<any>;
ogs$: Observable<any>;
constructor(
private websocketService: WebsocketService,
private seoService: SeoService,
@@ -41,12 +43,16 @@ export class AboutComponent implements OnInit {
ngOnInit() {
this.backendInfo$ = this.stateService.backendInfo$;
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project®\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`);
this.websocketService.want(['blocks']);
this.sponsors$ = this.apiService.getDonation$()
.pipe(
tap(() => this.goToAnchor())
);
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
tap(() => {
this.goToAnchor()
}),
share(),
)
this.translators$ = this.apiService.getTranslators$()
.pipe(
map((translators) => {
@@ -59,6 +65,9 @@ export class AboutComponent implements OnInit {
}),
tap(() => this.goToAnchor())
);
this.ogs$ = this.apiService.getOgs$();
this.allContributors$ = this.apiService.getContributor$().pipe(
map((contributors) => {
return {

View File

@@ -0,0 +1,21 @@
<div class="fee-graph" *ngIf="tx && estimate">
<div class="column">
<ng-container *ngFor="let bar of bars">
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
<div class="fill"></div>
<div class="line">
<p class="fee-rate">
<span class="label">{{ bar.label }}</span>
<span class="rate">
<app-fee-rate [fee]="bar.rate"></app-fee-rate>
</span>
</p>
</div>
<div class="spacer"></div>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
<div class="spacer"></div>
<div class="spacer"></div>
</div>
</ng-container>
</div>
</div>

View File

@@ -0,0 +1,157 @@
.fee-graph {
height: 100%;
min-width: 120px;
width: 120px;
max-height: 90vh;
margin-left: 4em;
margin-right: 1.5em;
padding-bottom: 63px;
.column {
width: 100%;
height: 100%;
position: relative;
background: #181b2d;
.bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.fill {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
opacity: 0.75;
pointer-events: none;
}
.fee {
font-size: 0.9em;
opacity: 0;
pointer-events: none;
}
.spacer {
width: 100%;
height: 1px;
flex-grow: 1;
pointer-events: none;
}
.line {
position: absolute;
right: 0;
top: 0;
left: -4.5em;
border-top: dashed white 1.5px;
.fee-rate {
width: 100%;
position: absolute;
left: 0;
right: 0.2em;
font-size: 0.8em;
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
margin: 0;
.label {
margin-right: .2em;
}
.rate .symbol {
color: white;
}
}
}
&.tx {
.fill {
background: #3bcc49;
}
.line {
.fee-rate {
top: 0;
}
}
.fee {
position: absolute;
opacity: 1;
z-index: 11;
}
}
&.target {
.fill {
background: #653b9c;
}
.fee {
position: absolute;
opacity: 1;
z-index: 11;
}
.line .fee-rate {
bottom: 2px;
}
}
&.max {
cursor: pointer;
.line .fee-rate {
.label {
opacity: 0;
}
bottom: 2px;
}
&.active, &:hover {
.fill {
background: #105fb0;
}
.line {
.fee-rate .label {
opacity: 1;
}
}
}
}
&:hover {
.fill {
z-index: 10;
}
.line {
z-index: 11;
}
.fee {
opacity: 1;
z-index: 12;
}
}
}
&:hover > .bar:not(:hover) {
&.target, &.max {
.fee {
opacity: 0;
}
.line .fee-rate .label {
opacity: 0;
}
}
&.max {
.fill {
background: none;
}
}
}
}
}

View File

@@ -0,0 +1,96 @@
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router';
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { AccelerationEstimate, RateOption } from './accelerate-preview.component';
interface GraphBar {
rate: number;
style: any;
class: 'tx' | 'target' | 'max';
label: string;
active?: boolean;
rateIndex?: number;
fee?: number;
}
@Component({
selector: 'app-accelerate-fee-graph',
templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'],
})
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
@Input() tx: Transaction;
@Input() estimate: AccelerationEstimate;
@Input() maxRateOptions: RateOption[] = [];
@Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
bars: GraphBar[] = [];
tooltipPosition = { x: 0, y: 0 };
ngOnInit(): void {
this.initGraph();
}
ngOnChanges(): void {
this.initGraph();
}
initGraph(): void {
if (!this.tx || !this.estimate) {
return;
}
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
const baseHeight = baseRate / maxRate;
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
return {
rate: option.rate,
style: this.getStyle(option.rate, maxRate, baseHeight),
class: 'max',
label: 'maximum',
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
}
});
bars.push({
rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
class: 'target',
label: 'next block',
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
});
bars.push({
rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0),
class: 'tx',
label: '',
fee: this.estimate.txSummary.effectiveFee,
});
this.bars = bars;
}
getStyle(rate, maxRate, base) {
const top = (rate / maxRate);
return {
height: `${(top - base) * 100}%`,
bottom: base ? `${base * 100}%` : '0',
}
}
onClick(event, bar): void {
if (bar.rateIndex != null) {
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
}
}

View File

@@ -0,0 +1,262 @@
<div class="row" *ngIf="showSuccess">
<div class="col" id="successAlert">
<div class="alert alert-success">
Transaction has now been submitted to mining pools for acceleration. You can track the progress <a class="alert-link" routerLink="/services/accelerator/history">here</a>.
</div>
</div>
</div>
<div class="row" *ngIf="error">
<div class="col" id="mempoolError">
<app-mempool-error [error]="error"></app-mempool-error>
</div>
</div>
<div class="accelerate-cols">
<ng-container *ngIf="!isMobile">
<app-accelerate-fee-graph
[tx]="tx"
[estimate]="estimate"
[maxRateOptions]="maxRateOptions"
[maxRateIndex]="selectFeeRateIndex"
(setUserBid)="setUserBid($event)"
></app-accelerate-fee-graph>
</ng-container>
<ng-container *ngIf="estimate">
<div [class]="{estimateDisabled: error}">
<h5>Your transaction</h5>
<div class="row">
<div class="col">
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor{{ estimate.txSummary.ancestorCount > 2 ? 's' : ''}}.
</small>
<table class="table table-borderless table-border table-dark table-accelerator">
<tbody>
<tr class="group-first">
<td class="item">
Virtual size
</td>
<td class="units" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
<td class="info">
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
<tr>
<td class="item">
In-band fees
</td>
<td class="units">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats|sats">sats</span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<br>
<h5>How much more are you willing to pay?</h5>
<div class="row">
<div class="col">
<small class="form-text text-muted mb-2">
Choose the maximum extra transaction fee you're willing to pay to get into the next block.<br>
If the estimated next block rate rises beyond this limit, we will automatically cancel your acceleration request.
</small>
<div class="form-group">
<div class="fee-card">
<div class="d-flex mb-0">
<ng-container *ngFor="let option of maxRateOptions">
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
<span class="fee">{{ option.fee | number }} <span class="symbol" i18n="shared.sats|sats">sats</span></span>
<span class="rate">~ <app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
</button>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<h5>Acceleration summary</h5>
<div class="row mb-3">
<div class="col">
<div class="table-toggle btn-group btn-group-toggle">
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'estimated'" (click)="showTable = 'estimated'">
<span>Estimated cost</span>
</div>
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'maximum'" (click)="showTable = 'maximum'">
<span>Maximum cost</span>
</div>
</div>
<table class="table table-borderless table-border table-dark table-accelerator">
<tbody>
<!-- ESTIMATED FEE -->
<ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first">
<td class="item">
Next block market rate
</td>
<td class="amt" style="font-size: 20px">
{{ estimate.targetFeeRate | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small>Estimated extra fee required</small></i>
</td>
<td class="amt">
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- USER MAX BID -->
<ng-container *ngIf="showTable === 'maximum'">
<tr class="group-first">
<td class="item">
Your maximum
</td>
<td class="amt" style="width: 45%; font-size: 20px">
~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small>The maximum extra transaction fee you could pay</small></i>
</td>
<td class="amt">
<span>
{{ userBid | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="userBid"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item">
Mempool Accelerator™ fees
</td>
</tr>
<tr class="info">
<td class="info">
<i><small>mempool.space fee</small></i>
</td>
<td class="amt">
+{{ estimate.mempoolBaseFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info">
<i><small>Transaction vsize fee</small></i>
</td>
<td class="amt">
+{{ estimate.vsizeFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #5E35B1" class="p-1 pl-0">
{{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td>
</tr>
</ng-container>
<!-- MAX COST -->
<ng-container *ngIf="showTable === 'maximum'">
<tr class="group-first">
<td class="item">
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #105fb0" class="p-1 pl-0">
{{ maxCost | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat">
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td>
</tr>
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item">
Available balance
</td>
<td class="amt">
{{ estimate.userBalance | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat">
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>
<div class="row mb-3" *ngIf="isLoggedIn()">
<div class="col">
<div class="d-flex justify-content-end">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,88 @@
.fee-card {
padding: 15px;
background-color: #1d1f31;
.feerate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.fee {
font-size: 1.2em;
}
.rate {
font-size: 0.9em;
.symbol {
color: white;
}
}
}
}
.btn-border {
border: solid 1px black;
background-color: #0c4a87;
}
.feerate.active {
background-color: #105fb0 !important;
opacity: 1;
border: 1px solid white !important;
}
.estimateDisabled {
opacity: 0.5;
pointer-events: none;
}
.table-toggle {
width: 100%;
margin-top: 0.5em;
}
.table-accelerator {
tr {
text-wrap: wrap;
td {
padding-top: 0;
padding-bottom: 0;
vertical-align: baseline;
}
&.group-first {
td {
padding-top: 0.75rem;
}
}
&.group-last {
td {
padding-bottom: 0.75rem;
}
}
}
td {
&:first-child {
width: 100vw;
}
&.info {
color: #6c757d;
}
&.amt {
text-align: right;
padding-right: 0.2em;
}
&.units {
padding-left: 0.2em;
white-space: nowrap;
}
}
}
.accelerate-cols {
display: flex;
flex-direction: row;
align-items: stretch;
margin-top: 1em;
}

View File

@@ -0,0 +1,205 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { nextRoundNumber } from '../../shared/common.utils';
export type AccelerationEstimate = {
txSummary: TxSummary;
nextBlockFee: number;
targetFeeRate: number;
userBalance: number;
enoughBalance: boolean;
cost: number;
mempoolBaseFee: number;
vsizeFee: number;
}
export type TxSummary = {
txid: string; // txid of the current transaction
effectiveVsize: number; // Total vsize of the dependency tree
effectiveFee: number; // Total fee of the dependency tree in sats
ancestorCount: number; // Number of ancestors
}
export interface RateOption {
fee: number;
rate: number;
index: number;
}
export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
@Component({
selector: 'app-accelerate-preview',
templateUrl: 'accelerate-preview.component.html',
styleUrls: ['accelerate-preview.component.scss']
})
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
@Input() tx: Transaction | undefined;
@Input() scrollEvent: boolean;
math = Math;
error = '';
showSuccess = false;
estimateSubscription: Subscription;
accelerationSubscription: Subscription;
estimate: any;
hasAncestors: boolean = false;
minExtraCost = 0;
minBidAllowed = 0;
maxBidAllowed = 0;
defaultBid = 0;
maxCost = 0;
userBid = 0;
selectFeeRateIndex = 1;
showTable: 'estimated' | 'maximum' = 'maximum';
isMobile: boolean = window.innerWidth <= 767.98;
maxRateOptions: RateOption[] = [];
constructor(
private apiService: ApiService,
private storageService: StorageService
) { }
ngOnDestroy(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
}
}
ngOnInit() {
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
this.estimate = undefined;
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
} else {
this.estimate = response.body;
if (!this.estimate) {
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
}
if (this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.error = `not_enough_balance`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
}
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
// Make min extra fee at least 50% of the current tx fee
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
return {
fee: this.minExtraCost * multiplier,
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
index,
};
});
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
this.userBid = this.defaultBid;
if (this.userBid < this.minBidAllowed) {
this.userBid = this.minBidAllowed;
} else if (this.userBid > this.maxBidAllowed) {
this.userBid = this.maxBidAllowed;
}
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
}
}
}),
catchError((response) => {
this.estimate = undefined;
this.error = response.error;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
return of(null);
})
).subscribe();
}
/**
* User changed his bid
*/
setUserBid({ fee, index }: { fee: number, index: number}) {
if (this.estimate) {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
}
}
/**
* Scroll to element id with or without setTimeout
*/
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
setTimeout(() => {
this.scrollToPreview(id, position);
}, 100);
}
scrollToPreview(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
acceleratePreviewAnchor.scrollIntoView({
behavior: 'smooth',
inline: position,
block: position,
});
}
}
/**
* Send acceleration request
*/
accelerate() {
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.apiService.accelerate$(
this.tx.txid,
this.userBid
).subscribe({
next: () => {
this.showSuccess = true;
this.scrollToPreviewWithTimeout('successAlert', 'center');
this.estimateSubscription.unsubscribe();
},
error: (response) => {
this.error = response.error;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
});
}
isLoggedIn() {
const auth = this.storageService.getAuth();
return auth !== null;
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}

View File

@@ -9,6 +9,7 @@ import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({
@@ -68,6 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)

View File

@@ -9,6 +9,7 @@ import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({
@@ -76,6 +77,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
return merge(
of(true),
@@ -91,6 +93,7 @@ export class AddressComponent implements OnInit, OnDestroy {
catchError((err) => {
this.isLoadingAddress = false;
this.error = err;
this.seoService.logSoft404();
console.log(err);
return of(null);
})
@@ -162,6 +165,7 @@ export class AddressComponent implements OnInit, OnDestroy {
(error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingAddress = false;
});
@@ -170,6 +174,11 @@ export class AddressComponent implements OnInit, OnDestroy {
this.addTransaction(tx);
});
this.stateService.mempoolRemovedTransactions$
.subscribe(tx => {
this.removeTransaction(tx);
});
this.stateService.blockTransactions$
.subscribe((transaction) => {
const tx = this.transactions.find((t) => t.txid === transaction.txid);
@@ -218,6 +227,30 @@ export class AddressComponent implements OnInit, OnDestroy {
return true;
}
removeTransaction(transaction: Transaction): boolean {
const index = this.transactions.findIndex(((tx) => tx.txid === transaction.txid));
if (index === -1) {
return false;
}
this.transactions.splice(index, 1);
this.transactions = this.transactions.slice();
this.txCount--;
transaction.vin.forEach((vin) => {
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
this.sent -= vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout?.scriptpubkey_address === this.address.address) {
this.received -= vout.value;
}
});
return true;
}
loadMore() {
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
return;

View File

@@ -86,6 +86,7 @@ export class AssetComponent implements OnInit, OnDestroy {
catchError((err) => {
this.isLoadingAsset = false;
this.error = err;
this.seoService.logSoft404();
console.log(err);
return of(null);
})
@@ -153,6 +154,7 @@ export class AssetComponent implements OnInit, OnDestroy {
(error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingAsset = false;
});

View File

@@ -40,6 +40,7 @@ export class AssetsNavComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
this.seoService.setDescription($localize`:@@meta.description.liquid.assets:Explore all the assets issued on the Liquid network like L-BTC, L-CAD, USDT, and more.`);
this.typeaheadSearchFn = this.typeaheadSearch;
this.searchForm = this.formBuilder.group({

View File

@@ -64,6 +64,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);

View File

@@ -65,6 +65,7 @@ export class BlockFeesGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fees:See the average mining fees earned per Bitcoin block visualized in BTC and USD over time.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -192,7 +193,7 @@ export class BlockFeesGraphComponent implements OnInit {
{
name: 'Fees ' + this.currency,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',

View File

@@ -61,6 +61,7 @@ export class BlockHealthGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-health:See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm.`);
this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);

View File

@@ -70,9 +70,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
this.initCanvas();
this.resizeCanvas();
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
}
}
ngOnChanges(changes): void {
@@ -147,7 +149,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) {
this.scene.update(add, remove, change, direction, resetLayout);
this.start();
@@ -195,10 +197,16 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null;
this.running = false;
this.gl = null;
}
handleContextRestored(event): void {
this.initCanvas();
if (this.canvas?.nativeElement) {
this.gl = this.canvas.nativeElement.getContext('webgl');
if (this.gl) {
this.initCanvas();
}
}
}
@HostListener('window:resize', ['$event'])
@@ -224,6 +232,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
compileShader(src, type): WebGLShader {
if (!this.gl) {
return;
}
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, src);
@@ -237,6 +248,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
buildShaderProgram(shaderInfo): WebGLProgram {
if (!this.gl) {
return;
}
const program = this.gl.createProgram();
shaderInfo.forEach((desc) => {
@@ -273,7 +287,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
now = performance.now();
}
// skip re-render if there's no change to the scene
if (this.scene) {
if (this.scene && this.gl) {
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);

View File

@@ -150,7 +150,7 @@ export default class BlockScene {
this.updateAll(startTime, 200, direction);
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
const startTime = performance.now();
const removed = this.removeBatch(remove, startTime, direction);
@@ -175,6 +175,7 @@ export default class BlockScene {
// update effective rates
change.forEach(tx => {
if (this.txs[tx.txid]) {
this.txs[tx.txid].acc = tx.acc;
this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize);
this.txs[tx.txid].rate = tx.rate;
this.txs[tx.txid].dirty = true;

View File

@@ -17,6 +17,7 @@ const auditColors = {
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'),
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
accelerated: hexToColor('8F5FF6'),
};
// convert from this class's update format to TxSprite's update format
@@ -37,8 +38,9 @@ export default class TxView implements TransactionStripped {
vsize: number;
value: number;
feerate: number;
acc?: boolean;
rate?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
scene?: BlockScene;
@@ -63,6 +65,7 @@ export default class TxView implements TransactionStripped {
this.vsize = tx.vsize;
this.value = tx.value;
this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
this.acc = tx.acc;
this.rate = tx.rate;
this.status = tx.status;
this.initialised = false;
@@ -199,6 +202,11 @@ export default class TxView implements TransactionStripped {
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
// Normal mode
if (!this.scene?.highlightingEnabled) {
if (this.acc) {
return auditColors.accelerated;
} else {
return feeLevelColor;
}
return feeLevelColor;
}
// Block audit
@@ -216,6 +224,8 @@ export default class TxView implements TransactionStripped {
return auditColors.added;
case 'selected':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'accelerated':
return auditColors.accelerated;
case 'found':
if (this.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
@@ -223,7 +233,11 @@ export default class TxView implements TransactionStripped {
return feeLevelColor;
}
default:
return feeLevelColor;
if (this.acc) {
return auditColors.accelerated;
} else {
return feeLevelColor;
}
}
}
}

View File

@@ -29,7 +29,8 @@
</td>
</tr>
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
<td class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
<td>
<app-fee-rate [fee]="effectiveRate"></app-fee-rate>
</td>
@@ -54,6 +55,7 @@
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
</ng-container>
</tr>
</tbody>

View File

@@ -21,6 +21,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
vsize = 1;
feeRate = 0;
effectiveRate;
acceleration;
tooltipPosition: Position = { x: 0, y: 0 };
@@ -53,6 +54,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
this.vsize = tx.vsize || 1;
this.feeRate = this.fee / this.vsize;
this.effectiveRate = tx.rate;
this.acceleration = tx.acc;
}
}
}

View File

@@ -63,6 +63,7 @@ export class BlockRewardsGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-rewards:See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -191,7 +192,7 @@ export class BlockRewardsGraphComponent implements OnInit {
{
name: 'Rewards ' + this.currency,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',

View File

@@ -60,6 +60,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
let firstRun = true;
this.seoService.setTitle($localize`:@@56fa1cd221491b6478998679cba2dc8d55ba330d:Block Sizes and Weights`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-sizes:See Bitcoin block sizes (MB) and block weights (weight units) visualized over time.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);

View File

@@ -0,0 +1,13 @@
<div class="block-wrapper">
<div class="block-container">
<app-block-overview-graph
#blockGraph
[isLoading]="false"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[disableSpinner]="true"
></app-block-overview-graph>
</div>
</div>

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