From 125b9e4525132304ca11c24eca871bd392adbc05 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 24 May 2023 15:49:35 -0400 Subject: [PATCH 01/33] Restore liquid max block weight to defaults --- production/mempool-frontend-config.liquid.json | 1 - 1 file changed, 1 deletion(-) diff --git a/production/mempool-frontend-config.liquid.json b/production/mempool-frontend-config.liquid.json index 6a7c79d52..1a4fc2998 100644 --- a/production/mempool-frontend-config.liquid.json +++ b/production/mempool-frontend-config.liquid.json @@ -11,7 +11,6 @@ "LIQUID_WEBSITE_URL": "https://liquid.network", "BISQ_WEBSITE_URL": "https://bisq.markets", "ITEMS_PER_PAGE": 25, - "BLOCK_WEIGHT_UNITS": 300000, "MEMPOOL_BLOCKS_AMOUNT": 2, "KEEP_BLOCKS_AMOUNT": 16 } From b3f90e298127ad0199ca3c0312fb3022ecfd4a28 Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Sun, 13 Aug 2023 08:23:57 +0900 Subject: [PATCH 02/33] Add mempool enterprise note to top-level readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd2e62478..9bc988970 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ It is an open-source project developed and operated for the benefit of the Bitco Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server. -**Most people should use a one-click install method.** Other install methods are meant for developers and others with experience managing servers. +Most people should use a one-click install method. + +Other install methods are meant for developers and others with experience managing servers. If you want support for your own production instance of Mempool, or if you'd like to have your own instance of Mempool run by the mempool.space team on their own global ISP infrastructure—check out Mempool Enterprise®. ## One-Click Installation From 102579baa972b80e08b637d034dfe54804cb7b9b Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Sun, 13 Aug 2023 13:00:47 +0900 Subject: [PATCH 03/33] Add enterprise note to production readme --- production/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/production/README.md b/production/README.md index 87b8bb0a1..8e325bb1b 100644 --- a/production/README.md +++ b/production/README.md @@ -2,7 +2,9 @@ These instructions are for setting up a serious production Mempool website for Bitcoin (mainnet, testnet, signet), Liquid (mainnet, testnet), and Bisq. -Again, this setup is no joke—home users should use [one of the other installation methods](../#installation-methods). Support is only provided to [enterprise sponsors](https://mempool.space/enterprise). +Again, this setup is no joke—home users should use [one of the other installation methods](../#installation-methods). Support is only provided to project sponsors through [Mempool Enterprise®](https://mempool.space/enterprise). + +You can also have the mempool.space team run a highly-performant and highly-available instance of Mempool for you on their own global ISP infrastructure. See Mempool Enterprise® for more details. ### Server Hardware From 0a918b8fa84270754ee3afa47c5dd9acca4c54e7 Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Sun, 13 Aug 2023 13:04:58 +0900 Subject: [PATCH 04/33] Add enterprise note to backend readme --- backend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/README.md b/backend/README.md index 6a0cb821c..0582aca8c 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,7 +2,7 @@ These instructions are mostly intended for developers. -If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool only provides support for custom setups to [enterprise sponsors](https://mempool.space/enterprise). +If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool only provides support for custom setups to project sponsors through [Mempool Enterprise®](https://mempool.space/enterprise). See other ways to set up Mempool on [the main README](/../../#installation-methods). From 83db6f642851dd460e801ef56ad7fa2fedd3215c Mon Sep 17 00:00:00 2001 From: orangesurf Date: Thu, 7 Dec 2023 09:28:36 +0000 Subject: [PATCH 05/33] Update License, about page and trademark policy with new registered trademarks --- LICENSE | 23 +++++++++++-------- .../app/components/about/about.component.html | 18 ++++++++++++--- .../trademark-policy.component.html | 7 ++++-- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/LICENSE b/LICENSE index 9f8592854..c592881f1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ -The Mempool Open Source Project -Copyright (c) 2019-2023 The Mempool Open Source Project Developers +The Mempool Open Source Project® +Copyright (c) 2019-2023 The Mempool Space K.K. and other shadowy super-coders This program is free software; you can redistribute it and/or modify it under the terms of (at your option) either: @@ -12,13 +12,18 @@ the terms of (at your option) either: Foundation, either version 3 of the License or any later version approved by a proxy statement published on . -However, this copyright license does not include an implied right or license to -use our trademarks: The Mempool Open Source Project®, mempool.space™, the -mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal -Logo™, the mempool Square Logo™, and the mempool Blocks logo™ are registered -trademarks or trademarks of Mempool Space K.K in Japan, the United States, -and/or other countries. See our full Trademark Policy and Guidelines for more -details, published on . +However, this copyright license does not include an implied right or license +to use any trademarks, service marks, logos, or trade names of Mempool Space K.K. +or any other contributor to The Mempool Open Source Project. + +The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool +Liquidity™, mempool.space®, the mempool Logo®, the mempool Square logo®, the mempool +Blocks logo™, the mempool.space Vertical Logo™, and the mempool.space Horizontal logo™ +are registered trademarks or trademarks of Mempool Space K.K in Japan, the United +States, and/or other countries. + +See our full Trademark Policy and Guidelines for more details, published on +. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index e3ad51116..926eb73a4 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -406,8 +406,15 @@
diff --git a/frontend/src/app/components/trademark-policy/trademark-policy.component.html b/frontend/src/app/components/trademark-policy/trademark-policy.component.html index 3a3da15dd..70b9d532d 100644 --- a/frontend/src/app/components/trademark-policy/trademark-policy.component.html +++ b/frontend/src/app/components/trademark-policy/trademark-policy.component.html @@ -8,7 +8,7 @@

Trademark Policy and Guidelines

The Mempool Open Source Project ®
-
Updated: July 19, 2021
+
Updated: December 7, 2023

@@ -56,6 +56,9 @@ Mempool Space K.K. The Mempool Open Source Project + Mempool Accelerator + Mempool Enterprise + Mempool Liquidity mempool.space @@ -304,7 +307,7 @@

Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:

-

“The Mempool Space K.K.™, The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”

+

"The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo®, the mempool Square logo®, the mempool Blocks logo™, the mempool.space Vertical Logo™, and the mempool.space Horizontal logo™; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."

  • What to Do When You See Abuse
  • From 5a2da751fa29ff2fc74f79723c723c4dc90da903 Mon Sep 17 00:00:00 2001 From: orangesurf Date: Thu, 7 Dec 2023 09:55:12 +0000 Subject: [PATCH 06/33] Remove TM & R from logo names --- LICENSE | 4 ++-- frontend/src/app/components/about/about.component.html | 4 ++-- .../trademark-policy/trademark-policy.component.html | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index c592881f1..d6d6ae2ce 100644 --- a/LICENSE +++ b/LICENSE @@ -17,8 +17,8 @@ to use any trademarks, service marks, logos, or trade names of Mempool Space K.K or any other contributor to The Mempool Open Source Project. The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool -Liquidity™, mempool.space®, the mempool Logo®, the mempool Square logo®, the mempool -Blocks logo™, the mempool.space Vertical Logo™, and the mempool.space Horizontal logo™ +Liquidity™, mempool.space®, the mempool Logo, the mempool Square logo, the mempool +Blocks logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 926eb73a4..2ab2d4e9c 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -410,7 +410,7 @@ and other shadowy super-coders

    - The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo®, the mempool Square logo®, the mempool Blocks logo™, the mempool.space Vertical Logo™, and the mempool.space Horizontal logo™; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. + The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.

    See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. @@ -436,7 +436,7 @@ Trademark Notice

    - Mempool Space K.K.™, The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo®, the mempool Square logo®, the mempool Blocks logo™, the mempool.space Vertical Logo™, and the mempool.space Horizontal logo™; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. + Mempool Space K.K.™, The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.

    While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. diff --git a/frontend/src/app/components/trademark-policy/trademark-policy.component.html b/frontend/src/app/components/trademark-policy/trademark-policy.component.html index 70b9d532d..b1fe4daac 100644 --- a/frontend/src/app/components/trademark-policy/trademark-policy.component.html +++ b/frontend/src/app/components/trademark-policy/trademark-policy.component.html @@ -307,7 +307,7 @@

    Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:

    -

    "The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo®, the mempool Square logo®, the mempool Blocks logo™, the mempool.space Vertical Logo™, and the mempool.space Horizontal logo™; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."

    +

    "The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."

  • What to Do When You See Abuse
  • From d38381af8b0a60170ff69ab805ac3e758dcdfa09 Mon Sep 17 00:00:00 2001 From: orangesurf Date: Fri, 8 Dec 2023 13:08:43 +0000 Subject: [PATCH 07/33] Changes following feedback --- frontend/src/app/components/about/about.component.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 2ab2d4e9c..34817b51c 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -409,9 +409,6 @@ Mempool Space K.K.
    and other shadowy super-coders -

    - The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. -

    See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>.

    @@ -436,7 +433,7 @@ Trademark Notice

    - Mempool Space K.K.™, The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. + The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.

    While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. From a4810b8be0438f512336a9ab93450f755d33fc09 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Fri, 17 Nov 2023 15:23:52 -0500 Subject: [PATCH 08/33] fix: incorrect `HTTP_PORT` docker compose field in docker README.md The override is [BACKEND_HTTP_PORT defined here](https://github.com/mempool/mempool/blob/59c513f2a52a96d125944b42230e1fb7b13b130e/docker/backend/start.sh#L7) --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 8dc1f264a..444324af8 100644 --- a/docker/README.md +++ b/docker/README.md @@ -124,7 +124,7 @@ Corresponding `docker-compose.yml` overrides: environment: MEMPOOL_NETWORK: "" MEMPOOL_BACKEND: "" - MEMPOOL_HTTP_PORT: "" + BACKEND_HTTP_PORT: "" MEMPOOL_SPAWN_CLUSTER_PROCS: "" MEMPOOL_API_URL_PREFIX: "" MEMPOOL_POLL_RATE_MS: "" From 88d2a3a50d35418bf05ab555c92f361bdbe271f0 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Fri, 17 Nov 2023 15:31:39 -0500 Subject: [PATCH 09/33] sign CLA for 0xBEEFCAF3 --- contributors/0xBEEFCAF3.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 contributors/0xBEEFCAF3.txt diff --git a/contributors/0xBEEFCAF3.txt b/contributors/0xBEEFCAF3.txt new file mode 100644 index 000000000..e00999be1 --- /dev/null +++ b/contributors/0xBEEFCAF3.txt @@ -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 November 17, 2023. + +Signed: 0xBEEFCAF3 From 05259e9f81419a51c76d64fec168bed0b1425353 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 02:42:17 +0000 Subject: [PATCH 10/33] Bump dtolnay/rust-toolchain Bumps [dtolnay/rust-toolchain](https://github.com/dtolnay/rust-toolchain) from f361669954a8ecfc00a3443f35f9ac8e610ffc06 to be73d7920c329f220ce78e0234b8f96b7ae60248. - [Release notes](https://github.com/dtolnay/rust-toolchain/releases) - [Commits](https://github.com/dtolnay/rust-toolchain/compare/f361669954a8ecfc00a3443f35f9ac8e610ffc06...be73d7920c329f220ce78e0234b8f96b7ae60248) --- updated-dependencies: - dependency-name: dtolnay/rust-toolchain dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d097318ca..0602e916a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain # Latest version available on this commit is 1.71.1 # Commit date is Aug 3, 2023 - uses: dtolnay/rust-toolchain@f361669954a8ecfc00a3443f35f9ac8e610ffc06 + uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 with: toolchain: ${{ steps.gettoolchain.outputs.toolchain }} From cb1d9753c8162fc1d90a1b7d9a1ed50b59ca6293 Mon Sep 17 00:00:00 2001 From: orangesurf Date: Mon, 11 Dec 2023 15:26:17 +0000 Subject: [PATCH 11/33] Changes following feedback --- LICENSE | 11 ++++++----- .../app/components/about/about.component.html | 5 +---- .../trademark-policy.component.html | 12 +++++++++--- .../src/resources/mempool-blocks-2-3-logo.jpeg | Bin 0 -> 12166 bytes .../src/resources/mempool-blocks-3-2-logo.jpeg | Bin 0 -> 10992 bytes 5 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 frontend/src/resources/mempool-blocks-2-3-logo.jpeg create mode 100644 frontend/src/resources/mempool-blocks-3-2-logo.jpeg diff --git a/LICENSE b/LICENSE index d6d6ae2ce..681dae366 100644 --- a/LICENSE +++ b/LICENSE @@ -16,11 +16,12 @@ However, this copyright license does not include an implied right or license to use any trademarks, service marks, logos, or trade names of Mempool Space K.K. or any other contributor to The Mempool Open Source Project. -The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool -Liquidity™, mempool.space®, the mempool Logo, the mempool Square logo, the mempool -Blocks logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo -are registered trademarks or trademarks of Mempool Space K.K in Japan, the United -States, and/or other countries. +The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, +Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full +Bitcoin ecosystem™, the mempool Logo, the mempool Square logo, the mempool Blocks +logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the +mempool.space Horizontal logo are registered trademarks or trademarks of Mempool +Space K.K in Japan, the United States, and/or other countries. See our full Trademark Policy and Guidelines for more details, published on . diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 34817b51c..b99329db2 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -409,9 +409,6 @@ Mempool Space K.K.
    and other shadowy super-coders -

    - See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. -

    The Mempool Open Source Project is free software; you can redistribute it and/or modify it under the terms of (at your option) either:

    @@ -433,7 +430,7 @@ Trademark Notice

    - The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. + The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.

    While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. diff --git a/frontend/src/app/components/trademark-policy/trademark-policy.component.html b/frontend/src/app/components/trademark-policy/trademark-policy.component.html index b1fe4daac..9d08c634d 100644 --- a/frontend/src/app/components/trademark-policy/trademark-policy.component.html +++ b/frontend/src/app/components/trademark-policy/trademark-policy.component.html @@ -60,6 +60,8 @@ Mempool Enterprise Mempool Liquidity mempool.space + Be your own explorer + Explore the full Bitcoin ecosystem @@ -92,11 +94,16 @@

    The mempool Square Logo



    - +

    The mempool Blocks Logo



    + +

    +

    The mempool Blocks 3 | 2 Logo

    +

    +
    @@ -307,8 +314,7 @@

    Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:

    -

    "The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."

    - +

    "The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."

  • What to Do When You See Abuse

  • diff --git a/frontend/src/resources/mempool-blocks-2-3-logo.jpeg b/frontend/src/resources/mempool-blocks-2-3-logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..9ec0525d38b36c3a4d68f2fecbf4e3bb45d721e2 GIT binary patch literal 12166 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 literal 0 HcmV?d00001 From d3f7190b7f68b6a269eae48bdbc4debad4e986be Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 12 Dec 2023 20:49:08 +0700 Subject: [PATCH 12/33] Acceleration preview loader --- .../accelerate-preview/accelerate-preview.component.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html index 455dffef4..2d2c9c3f3 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html @@ -25,7 +25,7 @@ > - +
    @@ -243,4 +243,9 @@
    -
    \ No newline at end of file + + + +
    +
    +
    \ No newline at end of file From 93c61f5ed3d3b0737416d74269189ace3367d2cb Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 13 Dec 2023 17:46:51 +0900 Subject: [PATCH 13/33] Change dual-licensed GPLv3/AGPLv3 to AGPLv3 only --- LICENSE | 15 ++++----------- .../src/app/components/about/about.component.html | 10 +--------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/LICENSE b/LICENSE index 681dae366..f65965c7c 100644 --- a/LICENSE +++ b/LICENSE @@ -2,15 +2,9 @@ The Mempool Open Source Project® Copyright (c) 2019-2023 The Mempool Space K.K. and other shadowy super-coders This program is free software; you can redistribute it and/or modify it under -the terms of (at your option) either: - - 1) the GNU Affero General Public License as published by the Free Software - Foundation, either version 3 of the License or any later version approved by a - proxy statement published on ; or - - 2) the GNU General Public License as published by the Free Software - Foundation, either version 3 of the License or any later version approved by a - proxy statement published on . +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License or any later version +approved by a proxy statement published on . However, this copyright license does not include an implied right or license to use any trademarks, service marks, logos, or trade names of Mempool Space K.K. @@ -31,5 +25,4 @@ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details. You should have received a copy of both the GNU Affero General Public License -and the GNU General Public License along with this program. If not, see -. +along with this program. If not, see . diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index b99329db2..98365e91f 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -410,16 +410,8 @@ and other shadowy super-coders

    - The Mempool Open Source Project is free software; you can redistribute it and/or modify it under the terms of (at your option) either:
    + The Mempool Open Source Project is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.

    -
      -
    • - 1) the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or
      -
    • -
    • - 2) the GNU General Public License as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.
      -
    • -

    This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.

    From 173bc127cb29e74468a85e9350eb426bd9554ad0 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 5 Dec 2023 06:54:31 +0000 Subject: [PATCH 14/33] Block viz filters proof of concept --- backend/src/api/common.ts | 107 +++++++++++++++++- backend/src/api/mempool-blocks.ts | 7 +- backend/src/api/mempool.ts | 3 + backend/src/mempool.interfaces.ts | 45 +++++++- .../block-overview-graph.component.ts | 9 ++ .../block-overview-graph/block-scene.ts | 17 ++- .../block-overview-graph/tx-view.ts | 4 +- .../block-overview-tooltip.component.ts | 2 +- .../mempool-block-view.component.html | 2 +- .../mempool-block-view.component.ts | 7 ++ .../mempool-block.component.html | 2 +- .../mempool-block/mempool-block.component.ts | 10 +- .../src/app/interfaces/node-api.interface.ts | 36 ++++++ .../src/app/interfaces/websocket.interface.ts | 1 + 14 files changed, 239 insertions(+), 13 deletions(-) diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index b6f8ab657..9bae2d906 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,9 +1,11 @@ import * as bitcoinjs from 'bitcoinjs-lib'; import { Request } from 'express'; -import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces'; +import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces'; import config from '../config'; import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { isIP } from 'net'; +import rbfCache from './rbf-cache'; +import transactionUtils from './transaction-utils'; export class Common { static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49' @@ -138,6 +140,109 @@ export class Common { return matches; } + static setSighashFlags(flags: bigint, signature: string): bigint { + switch(signature.slice(-2)) { + case '01': return flags | TransactionFlags.sighash_all; + case '02': return flags | TransactionFlags.sighash_none; + case '03': return flags | TransactionFlags.sighash_single; + case '81': return flags | TransactionFlags.sighash_all | TransactionFlags.sighash_acp; + case '82': return flags | TransactionFlags.sighash_none | TransactionFlags.sighash_acp; + case '83': return flags | TransactionFlags.sighash_single | TransactionFlags.sighash_acp; + default: return flags | TransactionFlags.sighash_default; // taproot only + } + } + + static getTransactionFlags(tx: TransactionExtended): number { + let flags = 0n; + if (tx.version === 1) { + flags |= TransactionFlags.v1; + } else if (tx.version === 2) { + flags |= TransactionFlags.v2; + } + const inValues = {}; + const outValues = {}; + let rbf = false; + for (const vin of tx.vin) { + if (vin.sequence < 0xfffffffe) { + rbf = true; + } + switch (vin.prevout?.scriptpubkey_type) { + case 'p2pk': { + flags |= TransactionFlags.p2pk; + flags = this.setSighashFlags(flags, vin.scriptsig); + } break; + case 'multisig': flags |= TransactionFlags.p2ms; break; + case 'p2pkh': flags |= TransactionFlags.p2pkh; break; + case 'p2sh': flags |= TransactionFlags.p2sh; break; + case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break; + case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; + case 'v1_p2tr': { + flags |= TransactionFlags.p2tr; + if (vin.witness.length > 2) { + const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - 2]); + if (asm?.includes('OP_0 OP_IF')) { + flags |= TransactionFlags.inscription; + } + } + } break; + } + inValues[vin.prevout?.value || Math.random()] = (inValues[vin.prevout?.value || Math.random()] || 0) + 1; + } + if (rbf) { + flags |= TransactionFlags.rbf; + } else { + flags |= TransactionFlags.no_rbf; + } + for (const vout of tx.vout) { + switch (vout.scriptpubkey_type) { + case 'p2pk': flags |= TransactionFlags.p2pk; break; + case 'multisig': { + flags |= TransactionFlags.p2ms; + // TODO - detect fake multisig data embedding + } break; + case 'p2pkh': flags |= TransactionFlags.p2pkh; break; + case 'p2sh': flags |= TransactionFlags.p2sh; break; + case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break; + case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; + case 'v1_p2tr': flags |= TransactionFlags.p2tr; break; + case 'op_return': flags |= TransactionFlags.op_return; break; + } + outValues[vout.value || Math.random()] = (outValues[vout.value || Math.random()] || 0) + 1; + } + if (tx.ancestors?.length) { + flags |= TransactionFlags.cpfp_child; + } + if (tx.descendants?.length) { + flags |= TransactionFlags.cpfp_parent; + } + if (rbfCache.getRbfTree(tx.txid)) { + flags |= TransactionFlags.replacement; + } + // fast but bad heuristic to detect possible coinjoins + // (at least 5 inputs and 5 outputs, less than half of which are unique amounts) + if (tx.vin.length >= 5 && tx.vout.length >= 5 && (Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 ) { + flags |= TransactionFlags.coinjoin; + } + // more than 5:1 input:output ratio + if (tx.vin.length / tx.vout.length >= 5) { + flags |= TransactionFlags.consolidation; + } + // less than 1:5 input:output ratio + if (tx.vin.length / tx.vout.length <= 0.2) { + flags |= TransactionFlags.batch_payout; + } + + return Number(flags); + } + + static classifyTransaction(tx: TransactionExtended): TransactionClassified { + const flags = this.getTransactionFlags(tx); + return { + ...this.stripTransaction(tx), + flags, + }; + } + static stripTransaction(tx: TransactionExtended): TransactionStripped { return { txid: tx.txid, diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 15f9b6cf7..a7f00f6e8 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -1,6 +1,6 @@ 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, PoolTag } from '../mempool.interfaces'; +import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified } from '../mempool.interfaces'; import { Common, OnlineFeeStatsCalculator } from './common'; import config from '../config'; import { Worker } from 'worker_threads'; @@ -169,7 +169,7 @@ class MempoolBlocks { private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] { const mempoolBlockDeltas: MempoolBlockDelta[] = []; for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { - let added: TransactionStripped[] = []; + let added: TransactionClassified[] = []; let removed: string[] = []; const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = []; if (mempoolBlocks[i] && !prevBlocks[i]) { @@ -582,6 +582,7 @@ class MempoolBlocks { const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); this.mempoolBlocks = mempoolBlocks; this.mempoolBlockDeltas = deltas; + } return mempoolBlocks; @@ -599,7 +600,7 @@ class MempoolBlocks { medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE), feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength), transactionIds: transactionIds, - transactions: transactions.map((tx) => Common.stripTransaction(tx)), + transactions: transactions.map((tx) => Common.classifyTransaction(tx)), }; } diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index fa13db418..a5bc8407a 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -100,6 +100,9 @@ class Mempool { if (this.mempoolCache[txid].order == null) { this.mempoolCache[txid].order = transactionUtils.txidToOrdering(txid); } + for (const vin of this.mempoolCache[txid].vin) { + transactionUtils.addInnerScriptsToVin(vin); + } count++; if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) { await redisCache.$addTransaction(this.mempoolCache[txid]); diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index cb212512c..f50274304 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -61,13 +61,13 @@ export interface MempoolBlock { export interface MempoolBlockWithTransactions extends MempoolBlock { transactionIds: string[]; - transactions: TransactionStripped[]; + transactions: TransactionClassified[]; } export interface MempoolBlockDelta { - added: TransactionStripped[]; + added: TransactionClassified[]; removed: string[]; - changed: { txid: string, rate: number | undefined }[]; + changed: { txid: string, rate: number | undefined, flags?: number }[]; } interface VinStrippedToScriptsig { @@ -190,6 +190,45 @@ export interface TransactionStripped { rate?: number; // effective fee rate } +export interface TransactionClassified extends TransactionStripped { + flags: number; +} + +// binary flags for transaction classification +export const TransactionFlags = { + // features + rbf: 0b00000001n, + no_rbf: 0b00000010n, + v1: 0b00000100n, + v2: 0b00001000n, + // address types + p2pk: 0b00000001_00000000n, + p2ms: 0b00000010_00000000n, + p2pkh: 0b00000100_00000000n, + p2sh: 0b00001000_00000000n, + p2wpkh: 0b00010000_00000000n, + p2wsh: 0b00100000_00000000n, + p2tr: 0b01000000_00000000n, + // behavior + cpfp_parent: 0b00000001_00000000_00000000n, + cpfp_child: 0b00000010_00000000_00000000n, + replacement: 0b00000100_00000000_00000000n, + // data + op_return: 0b00000001_00000000_00000000_00000000n, + fake_multisig: 0b00000010_00000000_00000000_00000000n, + inscription: 0b00000100_00000000_00000000_00000000n, + // heuristics + coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, + consolidation: 0b00000010_00000000_00000000_00000000_00000000n, + batch_payout: 0b00000100_00000000_00000000_00000000_00000000n, + // sighash + sighash_all: 0b00000001_00000000_00000000_00000000_00000000_00000000n, + sighash_none: 0b00000010_00000000_00000000_00000000_00000000_00000000n, + sighash_single: 0b00000100_00000000_00000000_00000000_00000000_00000000n, + sighash_default:0b00001000_00000000_00000000_00000000_00000000_00000000n, + sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, +}; + export interface BlockExtension { totalFees: number; medianFee: number; // median fee rate diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index 1fc173a2d..5eaee25a1 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -26,6 +26,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On @Input() mirrorTxid: string | void; @Input() unavailable: boolean = false; @Input() auditHighlighting: boolean = false; + @Input() filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n; @Input() blockConversion: Price; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); @@ -462,6 +463,14 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } } + setFilterFlags(flags: bigint | null): void { + if (this.scene) { + console.log('setting filter flags to ', this.filterFlags.toString(2)); + this.scene.setFilterFlags(flags); + this.start(); + } + } + onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) { const x = cssX * window.devicePixelRatio; const y = cssY * window.devicePixelRatio; diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index 77b7c2e05..b6cf0ce59 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -27,6 +27,7 @@ export default class BlockScene { configAnimationOffset: number | null; animationOffset: number; highlightingEnabled: boolean; + filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n; width: number; height: number; gridWidth: number; @@ -277,6 +278,20 @@ export default class BlockScene { this.animateUntil = Math.max(this.animateUntil, tx.update(update)); } + private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void { + if (tx.dirty || this.dirty) { + const txColor = tx.getColor(); + this.applyTxUpdate(tx, { + display: { + color: txColor + }, + duration: animate ? (duration || this.animationDuration) : 1, + start: startTime, + delay: animate ? delay : 0, + }); + } + } + private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left', animate: boolean = true): void { if (tx.dirty || this.dirty) { this.saveGridToScreenPosition(tx); @@ -325,7 +340,7 @@ export default class BlockScene { } else { this.applyTxUpdate(tx, { display: { - position: tx.screenPosition + position: tx.screenPosition, }, duration: animate ? this.animationDuration : 0, minDuration: animate ? (this.animationDuration / 2) : 0, diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index 4e2d855e6..da36b9880 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -1,9 +1,9 @@ import TxSprite from './tx-sprite'; import { FastVertexArray } from './fast-vertex-array'; -import { TransactionStripped } from '../../interfaces/websocket.interface'; import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; import { hexToColor } from './utils'; import BlockScene from './block-scene'; +import { TransactionStripped } from '../../interfaces/node-api.interface'; const hoverTransitionTime = 300; const defaultHoverColor = hexToColor('1bd8f4'); @@ -29,6 +29,7 @@ export default class TxView implements TransactionStripped { feerate: number; acc?: boolean; rate?: number; + bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; scene?: BlockScene; @@ -57,6 +58,7 @@ export default class TxView implements TransactionStripped { this.acc = tx.acc; this.rate = tx.rate; this.status = tx.status; + this.bigintFlags = tx.flags ? BigInt(tx.flags) : 0n; this.initialised = false; this.vertexArray = scene.vertexArray; diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts index 65d0f984c..a6e2a2697 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts @@ -1,7 +1,7 @@ import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core'; -import { TransactionStripped } from '../../interfaces/websocket.interface'; import { Position } from '../../components/block-overview-graph/sprite-types.js'; import { Price } from '../../services/price.service'; +import { TransactionStripped } from '../../interfaces/node-api.interface.js'; @Component({ selector: 'app-block-overview-tooltip', diff --git a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html index 9d51ff4e9..33ccf439b 100644 --- a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html +++ b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html @@ -1,5 +1,5 @@
    - +
    \ No newline at end of file diff --git a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.ts b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.ts index ebeb0801c..a671033cf 100644 --- a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.ts +++ b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.ts @@ -27,6 +27,7 @@ export class MempoolBlockViewComponent implements OnInit, OnDestroy { autofit: boolean = false; resolution: number = 80; index: number = 0; + filterFlags: bigint | null = 0n; routeParamsSubscription: Subscription; queryParamsSubscription: Subscription; @@ -38,6 +39,8 @@ export class MempoolBlockViewComponent implements OnInit, OnDestroy { ) { } ngOnInit(): void { + window['setFlags'] = this.setFilterFlags.bind(this); + this.websocketService.want(['blocks', 'mempool-blocks']); this.routeParamsSubscription = this.route.paramMap @@ -82,4 +85,8 @@ export class MempoolBlockViewComponent implements OnInit, OnDestroy { this.routeParamsSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe(); } + + setFilterFlags(flags: bigint | null) { + this.filterFlags = flags; + } } diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.html b/frontend/src/app/components/mempool-block/mempool-block.component.html index b089a6d74..af9225fe6 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.html +++ b/frontend/src/app/components/mempool-block/mempool-block.component.html @@ -46,7 +46,7 @@
    - +
    diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index c11bedacd..4b8d4de66 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { StateService } from '../../services/state.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { switchMap, map, tap, filter } from 'rxjs/operators'; @@ -21,6 +21,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { mempoolBlockTransactions$: Observable; ordinal$: BehaviorSubject = new BehaviorSubject(''); previewTx: TransactionStripped | void; + filterFlags: bigint | null = 0n; webGlEnabled: boolean; constructor( @@ -28,11 +29,13 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { public stateService: StateService, private seoService: SeoService, private websocketService: WebsocketService, + private cd: ChangeDetectorRef, ) { this.webGlEnabled = detectWebGL(); } ngOnInit(): void { + window['setFlags'] = this.setFilterFlags.bind(this); this.websocketService.want(['blocks', 'mempool-blocks']); this.mempoolBlock$ = this.route.paramMap @@ -89,6 +92,11 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { setTxPreview(event: TransactionStripped | void): void { this.previewTx = event; } + + setFilterFlags(flags: bigint | null) { + this.filterFlags = flags; + this.cd.markForCheck(); + } } function detectWebGL() { diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 862272330..3075491a8 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -180,10 +180,46 @@ export interface TransactionStripped { value: number; rate?: number; // effective fee rate acc?: boolean; + flags?: number | null; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; } +// binary flags for transaction classification +export const TransactionFlags = { + // features + rbf: 0b00000001n, + no_rbf: 0b00000010n, + v1: 0b00000100n, + v2: 0b00001000n, + // address types + p2pk: 0b00000001_00000000n, + p2ms: 0b00000010_00000000n, + p2pkh: 0b00000100_00000000n, + p2sh: 0b00001000_00000000n, + p2wpkh: 0b00010000_00000000n, + p2wsh: 0b00100000_00000000n, + p2tr: 0b01000000_00000000n, + // behavior + cpfp_parent: 0b00000001_00000000_00000000n, + cpfp_child: 0b00000010_00000000_00000000n, + replacement: 0b00000100_00000000_00000000n, + // data + op_return: 0b00000001_00000000_00000000_00000000n, + fake_multisig: 0b00000010_00000000_00000000_00000000n, + inscription: 0b00000100_00000000_00000000_00000000n, + // heuristics + coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, + consolidation: 0b00000010_00000000_00000000_00000000_00000000n, + batch_payout: 0b00000100_00000000_00000000_00000000_00000000n, + // sighash + sighash_all: 0b00000001_00000000_00000000_00000000_00000000_00000000n, + sighash_none: 0b00000010_00000000_00000000_00000000_00000000_00000000n, + sighash_single: 0b00000100_00000000_00000000_00000000_00000000_00000000n, + sighash_default:0b00001000_00000000_00000000_00000000_00000000_00000000n, + sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, +}; + export interface RbfTransaction extends TransactionStripped { rbf?: boolean; mined?: boolean, diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 1d0414de7..20bc42bde 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -90,6 +90,7 @@ export interface TransactionStripped { value: number; acc?: boolean; // is accelerated? rate?: number; // effective fee rate + flags?: number; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; } From e12f43e741665c282fb3d1bf3a702c351ad76cfb Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 13 Dec 2023 10:56:33 +0000 Subject: [PATCH 15/33] Add sighash filter flags --- backend/src/api/common.ts | 88 +++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 9bae2d906..00f4328ce 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -140,6 +140,65 @@ export class Common { return matches; } + static setSchnorrSighashFlags(flags: bigint, witness: string[]): bigint { + // no witness items + if (!witness?.length) { + return flags; + } + const hasAnnex = witness.length > 1 && witness[witness.length - 1].startsWith('50'); + if (witness?.length === (hasAnnex ? 2 : 1)) { + // keypath spend, signature is the only witness item + if (witness[0].length === 130) { + flags |= this.setSighashFlags(flags, witness[0]); + } else { + flags |= TransactionFlags.sighash_default; + } + } else { + // scriptpath spend, all items except for the script, control block and annex could be signatures + for (let i = 0; i < witness.length - (hasAnnex ? 3 : 2); i++) { + // handle probable signatures + if (witness[i].length === 130) { + flags |= this.setSighashFlags(flags, witness[i]); + } else if (witness[i].length === 128) { + flags |= TransactionFlags.sighash_default; + } + } + } + return flags; + } + + static isDERSig(w: string): boolean { + // heuristic to detect probable DER signatures + return (w.length >= 18 + && w.startsWith('30') // minimum DER signature length is 8 bytes + sighash flag (see https://mempool.space/testnet/tx/c6c232a36395fa338da458b86ff1327395a9afc28c5d2daa4273e410089fd433) + && ['01, 02, 03, 81, 82, 83'].includes(w.slice(-2)) // signature must end with a valid sighash flag + && (w.length === parseInt(w.slice(2, 4), 16) + 6) // second byte encodes the combined length of the R and S components + ); + } + + static setSegwitSighashFlags(flags: bigint, witness: string[]): bigint { + for (const w of witness) { + if (this.isDERSig(w)) { + flags |= this.setSighashFlags(flags, w); + } + } + return flags; + } + + static setLegacySighashFlags(flags: bigint, scriptsig_asm: string): bigint { + for (const item of scriptsig_asm.split(' ')) { + // skip op_codes + if (item.startsWith('OP_')) { + continue; + } + // check pushed data + if (this.isDERSig(item)) { + flags |= this.setSighashFlags(flags, item); + } + } + return flags; + } + static setSighashFlags(flags: bigint, signature: string): bigint { switch(signature.slice(-2)) { case '01': return flags | TransactionFlags.sighash_all; @@ -159,18 +218,16 @@ export class Common { } else if (tx.version === 2) { flags |= TransactionFlags.v2; } + const reusedAddresses: { [address: string ]: number } = {}; const inValues = {}; const outValues = {}; let rbf = false; for (const vin of tx.vin) { if (vin.sequence < 0xfffffffe) { - rbf = true; + rbf = true; } switch (vin.prevout?.scriptpubkey_type) { - case 'p2pk': { - flags |= TransactionFlags.p2pk; - flags = this.setSighashFlags(flags, vin.scriptsig); - } break; + case 'p2pk': flags |= TransactionFlags.p2pk; break; case 'multisig': flags |= TransactionFlags.p2ms; break; case 'p2pkh': flags |= TransactionFlags.p2pkh; break; case 'p2sh': flags |= TransactionFlags.p2sh; break; @@ -186,6 +243,19 @@ export class Common { } } break; } + + // sighash flags + if (vin.prevout?.scriptpubkey_type === 'v1_p2tr') { + flags |= this.setSchnorrSighashFlags(flags, vin.witness); + } else if (vin.witness) { + flags |= this.setSegwitSighashFlags(flags, vin.witness); + } else if (vin.scriptsig_asm) { + flags |= this.setLegacySighashFlags(flags, vin.scriptsig_asm); + } + + if (vin.prevout?.scriptpubkey_address) { + reusedAddresses[vin.prevout?.scriptpubkey_address] = (reusedAddresses[vin.prevout?.scriptpubkey_address] || 0) + 1; + } inValues[vin.prevout?.value || Math.random()] = (inValues[vin.prevout?.value || Math.random()] || 0) + 1; } if (rbf) { @@ -207,6 +277,9 @@ export class Common { case 'v1_p2tr': flags |= TransactionFlags.p2tr; break; case 'op_return': flags |= TransactionFlags.op_return; break; } + if (vout.scriptpubkey_address) { + reusedAddresses[vout.scriptpubkey_address] = (reusedAddresses[vout.scriptpubkey_address] || 0) + 1; + } outValues[vout.value || Math.random()] = (outValues[vout.value || Math.random()] || 0) + 1; } if (tx.ancestors?.length) { @@ -219,8 +292,9 @@ export class Common { flags |= TransactionFlags.replacement; } // fast but bad heuristic to detect possible coinjoins - // (at least 5 inputs and 5 outputs, less than half of which are unique amounts) - if (tx.vin.length >= 5 && tx.vout.length >= 5 && (Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 ) { + // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse) + const addressReuse = Object.values(reusedAddresses).reduce((acc, count) => Math.max(acc, count), 0) > 1; + if (!addressReuse && tx.vin.length >= 5 && tx.vout.length >= 5 && (Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 ) { flags |= TransactionFlags.coinjoin; } // more than 5:1 input:output ratio From 24dbe5d4ee45bf6fe7701d848c317392606660e3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 13 Dec 2023 10:59:28 +0000 Subject: [PATCH 16/33] Add block viz filter UI --- .../block-filters.component.html | 22 ++++ .../block-filters.component.scss | 104 ++++++++++++++++++ .../block-filters/block-filters.component.ts | 65 +++++++++++ .../block-overview-graph.component.html | 1 + .../block-overview-graph.component.ts | 55 +++++++-- .../block-overview-graph/block-scene.ts | 63 +---------- .../components/block-overview-graph/utils.ts | 72 ++++++++++++ .../mempool-block-overview.component.html | 1 + .../mempool-block-overview.component.ts | 1 + .../mempool-block-view.component.html | 2 +- .../mempool-block.component.html | 4 +- .../mempool-block/mempool-block.component.ts | 7 -- .../src/app/interfaces/node-api.interface.ts | 35 ------ frontend/src/app/shared/filters.utils.ts | 87 +++++++++++++++ frontend/src/app/shared/shared.module.ts | 3 + 15 files changed, 408 insertions(+), 114 deletions(-) create mode 100644 frontend/src/app/components/block-filters/block-filters.component.html create mode 100644 frontend/src/app/components/block-filters/block-filters.component.scss create mode 100644 frontend/src/app/components/block-filters/block-filters.component.ts create mode 100644 frontend/src/app/shared/filters.utils.ts diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html new file mode 100644 index 000000000..ff86e6b3b --- /dev/null +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -0,0 +1,22 @@ +
    +
    + +
    + + + +
    +
    +
    + +
    {{ group.label }}
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/frontend/src/app/components/block-filters/block-filters.component.scss b/frontend/src/app/components/block-filters/block-filters.component.scss new file mode 100644 index 000000000..ee9e7f4d3 --- /dev/null +++ b/frontend/src/app/components/block-filters/block-filters.component.scss @@ -0,0 +1,104 @@ +.block-filters { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + padding: 1em; + z-index: 10; + pointer-events: none; + + .filter-bar, .active-tags { + display: flex; + flex-direction: row; + align-items: center; + } + + .active-tags { + flex-wrap: wrap; + row-gap: 0.25em; + margin-left: 0.5em; + } + + .menu-toggle { + opacity: 0; + cursor: pointer; + color: white; + background: none; + border: solid 2px white; + border-radius: 0.35em; + pointer-events: all; + } + + .filter-menu { + h5 { + font-size: 0.8rem; + color: white; + margin: 0; + margin-top: 0.5em; + } + } + + .filter-group { + display: flex; + flex-direction: row; + flex-wrap: wrap; + row-gap: 0.25em; + margin-bottom: 0.5em; + } + + .filter-tag { + font-size: 0.9em; + background: #181b2daf; + border: solid 1px #105fb0; + color: white; + border-radius: 0.2rem; + padding: 0.2em 0.5em; + transition: background-color 300ms; + margin-right: 0.25em; + pointer-events: all; + + &.selected { + background-color: #105fb0; + } + } + + :host-context(.block-overview-graph:hover) &, &:hover, &:active { + .menu-toggle { + opacity: 0.5; + background: #181b2d; + + &:hover { + opacity: 1; + background: #181b2d7f; + } + } + + &.menu-open, &.filters-active { + .menu-toggle { + opacity: 1; + background: none; + + &:hover { + background: #181b2d7f; + } + } + } + } + + &.menu-open, &.filters-active { + .menu-toggle { + opacity: 1; + background: none; + + &:hover { + background: #181b2d7f; + } + } + } + + &.menu-open { + pointer-events: all; + background: #181b2d7f; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts new file mode 100644 index 000000000..fc154bb69 --- /dev/null +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -0,0 +1,65 @@ +import { Component, OnChanges, EventEmitter, Output, SimpleChanges, HostListener } from '@angular/core'; +import { FilterGroups, TransactionFilters, Filter, TransactionFlags } from '../../shared/filters.utils'; + + +@Component({ + selector: 'app-block-filters', + templateUrl: './block-filters.component.html', + styleUrls: ['./block-filters.component.scss'], +}) +export class BlockFiltersComponent implements OnChanges { + @Output() onFilterChanged: EventEmitter = new EventEmitter(); + + filters = TransactionFilters; + filterGroups = FilterGroups; + activeFilters: string[] = []; + filterFlags: { [key: string]: boolean } = {}; + menuOpen: boolean = false; + + constructor() {} + + ngOnChanges(changes: SimpleChanges): void { + + } + + toggleFilter(key): void { + const filter = this.filters[key]; + this.filterFlags[key] = !this.filterFlags[key]; + if (this.filterFlags[key]) { + // remove any other flags in the same toggle group + if (filter.toggle) { + this.activeFilters.forEach(f => { + if (this.filters[f].toggle === filter.toggle) { + this.filterFlags[f] = false; + } + }); + this.activeFilters = this.activeFilters.filter(f => this.filters[f].toggle !== filter.toggle); + } + // add new active filter + this.activeFilters.push(key); + } else { + // remove active filter + this.activeFilters = this.activeFilters.filter(f => f != key); + } + this.onFilterChanged.emit(this.getBooleanFlags()); + } + + getBooleanFlags(): bigint | null { + let flags = 0n; + for (const key of Object.keys(this.filterFlags)) { + if (this.filterFlags[key]) { + flags |= this.filters[key].flag; + } + } + return flags || null; + } + + @HostListener('document:click', ['$event']) + onClick(event): boolean { + // click away from menu + if (!event.target.closest('button')) { + this.menuOpen = false; + } + return true; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html index a625a0385..9f7408323 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html @@ -13,5 +13,6 @@ [auditEnabled]="auditHighlighting" [blockConversion]="blockConversion" > + diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index 5eaee25a1..716ba540e 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -8,6 +8,19 @@ import { Color, Position } from './sprite-types'; import { Price } from '../../services/price.service'; import { StateService } from '../../services/state.service'; import { Subscription } from 'rxjs'; +import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; + +const unmatchedOpacity = 0.2; +const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity)); +const unmatchedAuditFeeColors = defaultAuditFeeColors.map(c => setOpacity(c, unmatchedOpacity)); +const unmatchedMarginalFeeColors = defaultMarginalFeeColors.map(c => setOpacity(c, unmatchedOpacity)); +const unmatchedAuditColors = { + censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity), + missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity), + added: setOpacity(defaultAuditColors.added, unmatchedOpacity), + selected: setOpacity(defaultAuditColors.selected, unmatchedOpacity), + accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity), +}; @Component({ selector: 'app-block-overview-graph', @@ -26,7 +39,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On @Input() mirrorTxid: string | void; @Input() unavailable: boolean = false; @Input() auditHighlighting: boolean = false; - @Input() filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n; + @Input() showFilters: boolean = false; + @Input() filterFlags: bigint | null = null; @Input() blockConversion: Price; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); @@ -93,7 +107,18 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On if (changes.auditHighlighting) { this.setHighlightingEnabled(this.auditHighlighting); } - if (changes.overrideColor) { + if (changes.overrideColor && this.scene) { + this.scene.setColorFunction(this.overrideColors); + } + if ((changes.filterFlags || changes.showFilters) && this.scene) { + this.setFilterFlags(this.filterFlags); + } + } + + setFilterFlags(flags: bigint | null): void { + if (flags != null) { + this.scene.setColorFunction(this.getFilterColorFunction(flags)); + } else { this.scene.setColorFunction(this.overrideColors); } } @@ -375,6 +400,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On onPointerMove(event) { if (event.target === this.canvas.nativeElement) { this.setPreviewTx(event.offsetX, event.offsetY, false); + } else { + this.onPointerLeave(event); } } @@ -463,14 +490,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } } - setFilterFlags(flags: bigint | null): void { - if (this.scene) { - console.log('setting filter flags to ', this.filterFlags.toString(2)); - this.scene.setFilterFlags(flags); - this.start(); - } - } - onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) { const x = cssX * window.devicePixelRatio; const y = cssY * window.devicePixelRatio; @@ -483,6 +502,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On onTxHover(hoverId: string) { this.txHoverEvent.emit(hoverId); } + + getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { + return (tx: TxView) => { + if ((tx.bigintFlags & flags) === flags) { + return defaultColorFunction(tx); + } else { + return defaultColorFunction( + tx, + unmatchedFeeColors, + unmatchedAuditFeeColors, + unmatchedMarginalFeeColors, + unmatchedAuditColors + ); + } + }; + } } // WebGL shader attributes diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index b6cf0ce59..cb589527d 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -2,19 +2,7 @@ import { FastVertexArray } from './fast-vertex-array'; import TxView from './tx-view'; import { TransactionStripped } from '../../interfaces/websocket.interface'; import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; -import { feeLevels, mempoolFeeColors } from '../../app.constants'; -import { darken, desaturate, hexToColor } from './utils'; - -const feeColors = mempoolFeeColors.map(hexToColor); -const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); -const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); -const auditColors = { - censored: hexToColor('f344df'), - missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), - added: hexToColor('0099ff'), - selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), - accelerated: hexToColor('8F5FF6'), -}; +import { defaultColorFunction } from './utils'; export default class BlockScene { scene: { count: number, offset: { x: number, y: number}}; @@ -79,7 +67,7 @@ export default class BlockScene { } setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void { - this.getColor = colorFunction; + this.getColor = colorFunction || defaultColorFunction; this.dirty = true; if (this.initialised && this.scene) { this.updateColors(performance.now(), 50); @@ -280,7 +268,7 @@ export default class BlockScene { private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void { if (tx.dirty || this.dirty) { - const txColor = tx.getColor(); + const txColor = this.getColor(tx); this.applyTxUpdate(tx, { display: { color: txColor @@ -918,49 +906,4 @@ class BlockLayout { function feeRateDescending(a: TxView, b: TxView) { return b.feerate - a.feerate; -} - -function defaultColorFunction(tx: TxView): Color { - const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate - const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; - const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; - // Normal mode - if (!tx.scene?.highlightingEnabled) { - if (tx.acc) { - return auditColors.accelerated; - } else { - return feeLevelColor; - } - return feeLevelColor; - } - // Block audit - switch(tx.status) { - case 'censored': - return auditColors.censored; - case 'missing': - case 'sigop': - case 'rbf': - return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; - case 'fresh': - case 'freshcpfp': - return auditColors.missing; - case 'added': - return auditColors.added; - case 'selected': - return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; - case 'accelerated': - return auditColors.accelerated; - case 'found': - if (tx.context === 'projected') { - return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; - } else { - return feeLevelColor; - } - default: - if (tx.acc) { - return auditColors.accelerated; - } else { - return feeLevelColor; - } - } } \ No newline at end of file diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts index a0bb8e868..9c800ad85 100644 --- a/frontend/src/app/components/block-overview-graph/utils.ts +++ b/frontend/src/app/components/block-overview-graph/utils.ts @@ -1,4 +1,6 @@ +import { feeLevels, mempoolFeeColors } from '../../app.constants'; import { Color } from './sprite-types'; +import TxView from './tx-view'; export function hexToColor(hex: string): Color { return { @@ -25,5 +27,75 @@ export function darken(color: Color, amount: number): Color { g: color.g * amount, b: color.b * amount, a: color.a, + }; +} + +export function setOpacity(color: Color, opacity: number): Color { + return { + ...color, + a: opacity + }; +} + +// precomputed colors +export const defaultFeeColors = mempoolFeeColors.map(hexToColor); +export const defaultAuditFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); +export const defaultMarginalFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); +export const defaultAuditColors = { + censored: hexToColor('f344df'), + missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), + added: hexToColor('0099ff'), + selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), + accelerated: hexToColor('8F5FF6'), +}; + +export function defaultColorFunction( + tx: TxView, + feeColors: Color[] = defaultFeeColors, + auditFeeColors: Color[] = defaultAuditFeeColors, + marginalFeeColors: Color[] = defaultMarginalFeeColors, + auditColors: { [status: string]: Color } = defaultAuditColors +): Color { + const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate + const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; + const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; + // Normal mode + if (!tx.scene?.highlightingEnabled) { + if (tx.acc) { + return auditColors.accelerated; + } else { + return feeLevelColor; + } + return feeLevelColor; + } + // Block audit + switch(tx.status) { + case 'censored': + return auditColors.censored; + case 'missing': + case 'sigop': + case 'rbf': + return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; + case 'fresh': + case 'freshcpfp': + return auditColors.missing; + case 'added': + return auditColors.added; + case 'selected': + return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; + case 'accelerated': + return auditColors.accelerated; + case 'found': + if (tx.context === 'projected') { + return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; + } else { + return feeLevelColor; + } + default: + if (tx.acc) { + return auditColors.accelerated; + } else { + return feeLevelColor; + } } } \ No newline at end of file diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html index 1e0cba48c..85e7eebb1 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html @@ -5,6 +5,7 @@ [blockLimit]="stateService.blockVSize" [orientation]="timeLtr ? 'right' : 'left'" [flip]="true" + [showFilters]="showFilters" [overrideColors]="overrideColors" (txClickEvent)="onTxClick($event)" > diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 09eac989e..4beda043a 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -18,6 +18,7 @@ import TxView from '../block-overview-graph/tx-view'; }) export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { @Input() index: number; + @Input() showFilters: boolean = false; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @Output() txPreviewEvent = new EventEmitter(); diff --git a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html index 33ccf439b..2fafb31cd 100644 --- a/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html +++ b/frontend/src/app/components/mempool-block-view/mempool-block-view.component.html @@ -1,5 +1,5 @@
    - +
    \ No newline at end of file diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.html b/frontend/src/app/components/mempool-block/mempool-block.component.html index af9225fe6..d2aa1aed2 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.html +++ b/frontend/src/app/components/mempool-block/mempool-block.component.html @@ -46,7 +46,9 @@
    - +
    + +
    diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index 4b8d4de66..bb6e7791f 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -21,7 +21,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { mempoolBlockTransactions$: Observable; ordinal$: BehaviorSubject = new BehaviorSubject(''); previewTx: TransactionStripped | void; - filterFlags: bigint | null = 0n; webGlEnabled: boolean; constructor( @@ -35,7 +34,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { } ngOnInit(): void { - window['setFlags'] = this.setFilterFlags.bind(this); this.websocketService.want(['blocks', 'mempool-blocks']); this.mempoolBlock$ = this.route.paramMap @@ -92,11 +90,6 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { setTxPreview(event: TransactionStripped | void): void { this.previewTx = event; } - - setFilterFlags(flags: bigint | null) { - this.filterFlags = flags; - this.cd.markForCheck(); - } } function detectWebGL() { diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 3075491a8..e225eb758 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -185,41 +185,6 @@ export interface TransactionStripped { context?: 'projected' | 'actual'; } -// binary flags for transaction classification -export const TransactionFlags = { - // features - rbf: 0b00000001n, - no_rbf: 0b00000010n, - v1: 0b00000100n, - v2: 0b00001000n, - // address types - p2pk: 0b00000001_00000000n, - p2ms: 0b00000010_00000000n, - p2pkh: 0b00000100_00000000n, - p2sh: 0b00001000_00000000n, - p2wpkh: 0b00010000_00000000n, - p2wsh: 0b00100000_00000000n, - p2tr: 0b01000000_00000000n, - // behavior - cpfp_parent: 0b00000001_00000000_00000000n, - cpfp_child: 0b00000010_00000000_00000000n, - replacement: 0b00000100_00000000_00000000n, - // data - op_return: 0b00000001_00000000_00000000_00000000n, - fake_multisig: 0b00000010_00000000_00000000_00000000n, - inscription: 0b00000100_00000000_00000000_00000000n, - // heuristics - coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, - consolidation: 0b00000010_00000000_00000000_00000000_00000000n, - batch_payout: 0b00000100_00000000_00000000_00000000_00000000n, - // sighash - sighash_all: 0b00000001_00000000_00000000_00000000_00000000_00000000n, - sighash_none: 0b00000010_00000000_00000000_00000000_00000000_00000000n, - sighash_single: 0b00000100_00000000_00000000_00000000_00000000_00000000n, - sighash_default:0b00001000_00000000_00000000_00000000_00000000_00000000n, - sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, -}; - export interface RbfTransaction extends TransactionStripped { rbf?: boolean; mined?: boolean, diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts new file mode 100644 index 000000000..f7e2f91c1 --- /dev/null +++ b/frontend/src/app/shared/filters.utils.ts @@ -0,0 +1,87 @@ +export interface Filter { + key: string, + label: string, + flag: bigint, + toggle?: string, + group?: string, +} + +// binary flags for transaction classification +export const TransactionFlags = { + // features + rbf: 0b00000001n, + no_rbf: 0b00000010n, + v1: 0b00000100n, + v2: 0b00001000n, + multisig: 0b00010000n, + // address types + p2pk: 0b00000001_00000000n, + p2ms: 0b00000010_00000000n, + p2pkh: 0b00000100_00000000n, + p2sh: 0b00001000_00000000n, + p2wpkh: 0b00010000_00000000n, + p2wsh: 0b00100000_00000000n, + p2tr: 0b01000000_00000000n, + // behavior + cpfp_parent: 0b00000001_00000000_00000000n, + cpfp_child: 0b00000010_00000000_00000000n, + replacement: 0b00000100_00000000_00000000n, + // data + op_return: 0b00000001_00000000_00000000_00000000n, + fake_multisig: 0b00000010_00000000_00000000_00000000n, + inscription: 0b00000100_00000000_00000000_00000000n, + // heuristics + coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, + consolidation: 0b00000010_00000000_00000000_00000000_00000000n, + batch_payout: 0b00000100_00000000_00000000_00000000_00000000n, + // sighash + sighash_all: 0b00000001_00000000_00000000_00000000_00000000_00000000n, + sighash_none: 0b00000010_00000000_00000000_00000000_00000000_00000000n, + sighash_single: 0b00000100_00000000_00000000_00000000_00000000_00000000n, + sighash_default:0b00001000_00000000_00000000_00000000_00000000_00000000n, + sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n, +}; + +export const TransactionFilters: { [key: string]: Filter } = { + // features + rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf' }, + no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf' }, + v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' }, + v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' }, + multisig: { key: 'multisig', label: 'Multisig', flag: TransactionFlags.multisig }, + // address types + p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk }, + p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms }, + p2pkh: { key: 'p2pkh', label: 'P2PKH', flag: TransactionFlags.p2pkh }, + p2sh: { key: 'p2sh', label: 'P2SH', flag: TransactionFlags.p2sh }, + p2wpkh: { key: 'p2wpkh', label: 'P2WPKH', flag: TransactionFlags.p2wpkh }, + p2wsh: { key: 'p2wsh', label: 'P2WSH', flag: TransactionFlags.p2wsh }, + p2tr: { key: 'p2tr', label: 'Taproot', flag: TransactionFlags.p2tr }, + // behavior + cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent }, + cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child }, + replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement }, + // data + op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return }, + // fake_multisig: { key: 'fake_multisig', label: 'Fake multisig', flag: TransactionFlags.fake_multisig }, + inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription }, + // heuristics + coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin }, + consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation }, + batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout }, + // sighash + sighash_all: { key: 'sighash_all', label: 'sighash_all', flag: TransactionFlags.sighash_all }, + sighash_none: { key: 'sighash_none', label: 'sighash_none', flag: TransactionFlags.sighash_none }, + sighash_single: { key: 'sighash_single', label: 'sighash_single', flag: TransactionFlags.sighash_single }, + sighash_default: { key: 'sighash_default', label: 'sighash_default', flag: TransactionFlags.sighash_default }, + sighash_acp: { key: 'sighash_acp', label: 'sighash_anyonecanpay', flag: TransactionFlags.sighash_acp }, +}; + +export const FilterGroups: { label: string, filters: Filter[]}[] = [ + { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'multisig'] }, + { label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] }, + { label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement'] }, + { label: 'Data', filters: ['op_return', 'fake_multisig', 'inscription'] }, + { label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] }, + { label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] }, +].map(group => ({ label: group.label, filters: group.filters.map(filter => TransactionFilters[filter] || null).filter(f => f != null) })); \ No newline at end of file diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 52123f995..9bcfb932c 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -44,6 +44,7 @@ import { StartComponent } from '../components/start/start.component'; import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; +import { BlockFiltersComponent } from '../components/block-filters/block-filters.component'; import { AddressComponent } from '../components/address/address.component'; import { SearchFormComponent } from '../components/search-form/search-form.component'; import { AddressLabelsComponent } from '../components/address-labels/address-labels.component'; @@ -141,6 +142,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir StartComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, + BlockFiltersComponent, TransactionsListComponent, AddressComponent, SearchFormComponent, @@ -266,6 +268,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir StartComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, + BlockFiltersComponent, TransactionsListComponent, AddressComponent, SearchFormComponent, From 5777561744d8e0525c8f397ff1cd345c8cfe9d23 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 13 Dec 2023 11:04:45 +0000 Subject: [PATCH 17/33] Tidy up block filter code, disable multisig flag --- .../block-filters/block-filters.component.ts | 12 +++--------- frontend/src/app/shared/filters.utils.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts index fc154bb69..97b23a4db 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.ts +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -1,5 +1,5 @@ -import { Component, OnChanges, EventEmitter, Output, SimpleChanges, HostListener } from '@angular/core'; -import { FilterGroups, TransactionFilters, Filter, TransactionFlags } from '../../shared/filters.utils'; +import { Component, EventEmitter, Output, HostListener } from '@angular/core'; +import { FilterGroups, TransactionFilters } from '../../shared/filters.utils'; @Component({ @@ -7,7 +7,7 @@ import { FilterGroups, TransactionFilters, Filter, TransactionFlags } from '../. templateUrl: './block-filters.component.html', styleUrls: ['./block-filters.component.scss'], }) -export class BlockFiltersComponent implements OnChanges { +export class BlockFiltersComponent { @Output() onFilterChanged: EventEmitter = new EventEmitter(); filters = TransactionFilters; @@ -16,12 +16,6 @@ export class BlockFiltersComponent implements OnChanges { filterFlags: { [key: string]: boolean } = {}; menuOpen: boolean = false; - constructor() {} - - ngOnChanges(changes: SimpleChanges): void { - - } - toggleFilter(key): void { const filter = this.filters[key]; this.filterFlags[key] = !this.filterFlags[key]; diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts index f7e2f91c1..72cb5976a 100644 --- a/frontend/src/app/shared/filters.utils.ts +++ b/frontend/src/app/shared/filters.utils.ts @@ -43,13 +43,13 @@ export const TransactionFlags = { }; export const TransactionFilters: { [key: string]: Filter } = { - // features + /* features */ rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf' }, no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf' }, v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' }, v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' }, - multisig: { key: 'multisig', label: 'Multisig', flag: TransactionFlags.multisig }, - // address types + // multisig: { key: 'multisig', label: 'Multisig', flag: TransactionFlags.multisig }, + /* address types */ p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk }, p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms }, p2pkh: { key: 'p2pkh', label: 'P2PKH', flag: TransactionFlags.p2pkh }, @@ -57,19 +57,19 @@ export const TransactionFilters: { [key: string]: Filter } = { p2wpkh: { key: 'p2wpkh', label: 'P2WPKH', flag: TransactionFlags.p2wpkh }, p2wsh: { key: 'p2wsh', label: 'P2WSH', flag: TransactionFlags.p2wsh }, p2tr: { key: 'p2tr', label: 'Taproot', flag: TransactionFlags.p2tr }, - // behavior + /* behavior */ cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent }, cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child }, replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement }, - // data + /* data */ op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return }, // fake_multisig: { key: 'fake_multisig', label: 'Fake multisig', flag: TransactionFlags.fake_multisig }, inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription }, - // heuristics + /* heuristics */ coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin }, consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation }, batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout }, - // sighash + /* sighash */ sighash_all: { key: 'sighash_all', label: 'sighash_all', flag: TransactionFlags.sighash_all }, sighash_none: { key: 'sighash_none', label: 'sighash_none', flag: TransactionFlags.sighash_none }, sighash_single: { key: 'sighash_single', label: 'sighash_single', flag: TransactionFlags.sighash_single }, From c019355c9fb7a17b12efb453ca2c600f8d46ef1b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 13 Dec 2023 11:29:10 +0000 Subject: [PATCH 18/33] Adapt block filter UI for small screens --- .../block-filters.component.html | 11 +++++-- .../block-filters.component.scss | 24 ++++++++++++++ .../block-filters/block-filters.component.ts | 15 +++++++-- .../block-overview-graph.component.html | 2 +- frontend/src/app/shared/filters.utils.ts | 31 ++++++++++--------- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html index ff86e6b3b..90b66ddc3 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.html +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -1,4 +1,4 @@ -
    +
    -
    +
    {{ group.label }}
    @@ -19,4 +19,11 @@
    +
    + + + + + +
    \ No newline at end of file diff --git a/frontend/src/app/components/block-filters/block-filters.component.scss b/frontend/src/app/components/block-filters/block-filters.component.scss index ee9e7f4d3..20b565293 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.scss +++ b/frontend/src/app/components/block-filters/block-filters.component.scss @@ -101,4 +101,28 @@ pointer-events: all; background: #181b2d7f; } + + &.small { + .filter-tag { + font-size: 0.8em; + } + } + + &.vsmall { + .filter-menu { + margin-top: 0.25em; + h5 { + display: none; + } + } + .filter-tag { + font-size: 0.7em; + } + } + + &.tiny { + .filter-tag { + font-size: 0.5em; + } + } } \ No newline at end of file diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts index 97b23a4db..ce0dd76ab 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.ts +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Output, HostListener } from '@angular/core'; +import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core'; import { FilterGroups, TransactionFilters } from '../../shared/filters.utils'; @@ -7,7 +7,8 @@ import { FilterGroups, TransactionFilters } from '../../shared/filters.utils'; templateUrl: './block-filters.component.html', styleUrls: ['./block-filters.component.scss'], }) -export class BlockFiltersComponent { +export class BlockFiltersComponent implements OnChanges { + @Input() cssWidth: number = 800; @Output() onFilterChanged: EventEmitter = new EventEmitter(); filters = TransactionFilters; @@ -16,6 +17,16 @@ export class BlockFiltersComponent { filterFlags: { [key: string]: boolean } = {}; menuOpen: boolean = false; + constructor( + private cd: ChangeDetectorRef, + ) {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes.cssWidth) { + this.cd.markForCheck(); + } + } + toggleFilter(key): void { const filter = this.filters[key]; this.filterFlags[key] = !this.filterFlags[key]; diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html index 9f7408323..251b84a73 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html @@ -13,6 +13,6 @@ [auditEnabled]="auditHighlighting" [blockConversion]="blockConversion" > - +
    diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts index 72cb5976a..4a8cb6a15 100644 --- a/frontend/src/app/shared/filters.utils.ts +++ b/frontend/src/app/shared/filters.utils.ts @@ -4,6 +4,7 @@ export interface Filter { flag: bigint, toggle?: string, group?: string, + important?: boolean, } // binary flags for transaction classification @@ -44,29 +45,29 @@ export const TransactionFlags = { export const TransactionFilters: { [key: string]: Filter } = { /* features */ - rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf' }, - no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf' }, + rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true }, + no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf', important: true }, v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' }, v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' }, // multisig: { key: 'multisig', label: 'Multisig', flag: TransactionFlags.multisig }, /* address types */ - p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk }, - p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms }, - p2pkh: { key: 'p2pkh', label: 'P2PKH', flag: TransactionFlags.p2pkh }, - p2sh: { key: 'p2sh', label: 'P2SH', flag: TransactionFlags.p2sh }, - p2wpkh: { key: 'p2wpkh', label: 'P2WPKH', flag: TransactionFlags.p2wpkh }, - p2wsh: { key: 'p2wsh', label: 'P2WSH', flag: TransactionFlags.p2wsh }, - p2tr: { key: 'p2tr', label: 'Taproot', flag: TransactionFlags.p2tr }, + p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true }, + p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true }, + p2pkh: { key: 'p2pkh', label: 'P2PKH', flag: TransactionFlags.p2pkh, important: true }, + p2sh: { key: 'p2sh', label: 'P2SH', flag: TransactionFlags.p2sh, important: true }, + p2wpkh: { key: 'p2wpkh', label: 'P2WPKH', flag: TransactionFlags.p2wpkh, important: true }, + p2wsh: { key: 'p2wsh', label: 'P2WSH', flag: TransactionFlags.p2wsh, important: true }, + p2tr: { key: 'p2tr', label: 'Taproot', flag: TransactionFlags.p2tr, important: true }, /* behavior */ - cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent }, - cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child }, - replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement }, + cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent, important: true }, + cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child, important: true }, + replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true }, /* data */ - op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return }, + op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true }, // fake_multisig: { key: 'fake_multisig', label: 'Fake multisig', flag: TransactionFlags.fake_multisig }, - inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription }, + inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription, important: true }, /* heuristics */ - coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin }, + coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin, important: true }, consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation }, batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout }, /* sighash */ From 7cb58ed98854fea8941882762add04f17beaf136 Mon Sep 17 00:00:00 2001 From: takuabonn Date: Wed, 13 Dec 2023 21:48:41 +0900 Subject: [PATCH 19/33] update to use cachedRequest --- .../app/lightning/lightning-api.service.ts | 44 ++++++++++++++++++- .../nodes-networks-chart.component.ts | 4 +- .../lightning-statistics-chart.component.ts | 4 +- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index fda93d446..ee55fb75d 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs'; import { StateService } from '../services/state.service'; import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface'; @@ -9,6 +9,8 @@ import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesP }) export class LightningApiService { private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet + + private requestCache = new Map, expiry: number }>; constructor( private httpClient: HttpClient, @@ -23,6 +25,46 @@ export class LightningApiService { }); } + private generateCacheKey(functionName: string, params: any[]): string { + return functionName + JSON.stringify(params); + } + + // delete expired cache entries + private cleanExpiredCache(): void { + this.requestCache.forEach((value, key) => { + if (value.expiry < Date.now()) { + this.requestCache.delete(key); + } + }); + } + + cachedRequest Observable>( + apiFunction: F, + expireAfter: number, // in ms + ...params: Parameters + ): Observable { + this.cleanExpiredCache(); + + const cacheKey = this.generateCacheKey(apiFunction.name, params); + if (!this.requestCache.has(cacheKey)) { + const subject = new BehaviorSubject(null); + this.requestCache.set(cacheKey, { subject, expiry: Date.now() + expireAfter }); + + apiFunction.bind(this)(...params).pipe( + tap(data => { + subject.next(data as T); + }), + catchError((error) => { + subject.error(error); + return of(null); + }), + shareReplay(1), + ).subscribe(); + } + + return this.requestCache.get(cacheKey).subject.asObservable().pipe(filter(val => val !== null), take(1)); + } + getNode$(publicKey: string): Observable { return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey); } diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index 7352d884d..30f786b16 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -82,9 +82,9 @@ export class NodesNetworksChartComponent implements OnInit { firstRun = false; this.miningWindowPreference = timespan; this.isLoading = true; - return this.lightningApiService.listStatistics$(timespan) + return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan) .pipe( - tap((response) => { + tap((response:any) => { const data = response.body; const chartData = { tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]), diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts index 7417a35cd..0e4f66ca0 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -81,9 +81,9 @@ export class LightningStatisticsChartComponent implements OnInit { firstRun = false; this.miningWindowPreference = timespan; this.isLoading = true; - return this.lightningApiService.listStatistics$(timespan) + return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan) .pipe( - tap((response) => { + tap((response:any) => { const data = response.body; this.prepareChartOptions({ channel_count: data.map(val => [val.added * 1000, val.channel_count]), From 1fb3de9dc3b15aaf8cbad31d0fbc8268d468159e Mon Sep 17 00:00:00 2001 From: takuabonn Date: Wed, 13 Dec 2023 23:26:14 +0900 Subject: [PATCH 20/33] update contributors --- contributors/takuabonn.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 contributors/takuabonn.txt diff --git a/contributors/takuabonn.txt b/contributors/takuabonn.txt new file mode 100644 index 000000000..331019e91 --- /dev/null +++ b/contributors/takuabonn.txt @@ -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 December 13, 2023. + +Signed: takuabonn \ No newline at end of file From ce195c913397dfca81a120f005b2e2175f19d238 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 13 Dec 2023 16:15:55 +0000 Subject: [PATCH 21/33] Fix ECDSA DER signature detection --- backend/src/api/common.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 00f4328ce..42dae7eb0 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -171,8 +171,8 @@ export class Common { // heuristic to detect probable DER signatures return (w.length >= 18 && w.startsWith('30') // minimum DER signature length is 8 bytes + sighash flag (see https://mempool.space/testnet/tx/c6c232a36395fa338da458b86ff1327395a9afc28c5d2daa4273e410089fd433) - && ['01, 02, 03, 81, 82, 83'].includes(w.slice(-2)) // signature must end with a valid sighash flag - && (w.length === parseInt(w.slice(2, 4), 16) + 6) // second byte encodes the combined length of the R and S components + && ['01', '02', '03', '81', '82', '83'].includes(w.slice(-2)) // signature must end with a valid sighash flag + && (w.length === (2 * parseInt(w.slice(2, 4), 16)) + 6) // second byte encodes the combined length of the R and S components ); } From 512589dc79fde70a1dec5f6ed443b4b1d84aa789 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 14 Dec 2023 11:26:17 +0000 Subject: [PATCH 22/33] Add fake pubkey filter --- backend/src/api/common.ts | 32 ++++++++-- backend/src/mempool.interfaces.ts | 2 +- backend/src/utils/secp256k1.ts | 74 ++++++++++++++++++++++++ frontend/src/app/shared/filters.utils.ts | 6 +- 4 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 backend/src/utils/secp256k1.ts diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 42dae7eb0..751bab5a3 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -6,6 +6,7 @@ import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { isIP } from 'net'; import rbfCache from './rbf-cache'; import transactionUtils from './transaction-utils'; +import { isPoint } from '../utils/secp256k1'; export class Common { static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49' @@ -211,6 +212,15 @@ export class Common { } } + static isBurnKey(pubkey: string): boolean { + return [ + '022222222222222222222222222222222222222222222222222222222222222222', + '033333333333333333333333333333333333333333333333333333333333333333', + '020202020202020202020202020202020202020202020202020202020202020202', + '030303030303030303030303030303030303030303030303030303030303030303', + ].includes(pubkey); + } + static getTransactionFlags(tx: TransactionExtended): number { let flags = 0n; if (tx.version === 1) { @@ -249,8 +259,8 @@ export class Common { flags |= this.setSchnorrSighashFlags(flags, vin.witness); } else if (vin.witness) { flags |= this.setSegwitSighashFlags(flags, vin.witness); - } else if (vin.scriptsig_asm) { - flags |= this.setLegacySighashFlags(flags, vin.scriptsig_asm); + } else if (vin.scriptsig?.length) { + flags |= this.setLegacySighashFlags(flags, vin.scriptsig_asm || transactionUtils.convertScriptSigAsm(vin.scriptsig)); } if (vin.prevout?.scriptpubkey_address) { @@ -263,12 +273,23 @@ export class Common { } else { flags |= TransactionFlags.no_rbf; } + let hasFakePubkey = false; for (const vout of tx.vout) { switch (vout.scriptpubkey_type) { - case 'p2pk': flags |= TransactionFlags.p2pk; break; + case 'p2pk': { + flags |= TransactionFlags.p2pk; + // detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve) + hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey.slice(2, -2)); + } break; case 'multisig': { flags |= TransactionFlags.p2ms; - // TODO - detect fake multisig data embedding + // detect fake pubkeys (i.e. not valid DER points on the secp256k1 curve) + const asm = vout.scriptpubkey_asm || transactionUtils.convertScriptSigAsm(vout.scriptpubkey); + for (const key of (asm?.split(' ') || [])) { + if (!hasFakePubkey && !key.startsWith('OP_')) { + hasFakePubkey = hasFakePubkey || this.isBurnKey(key) || !isPoint(key); + } + } } break; case 'p2pkh': flags |= TransactionFlags.p2pkh; break; case 'p2sh': flags |= TransactionFlags.p2sh; break; @@ -282,6 +303,9 @@ export class Common { } outValues[vout.value || Math.random()] = (outValues[vout.value || Math.random()] || 0) + 1; } + if (hasFakePubkey) { + flags |= TransactionFlags.fake_pubkey; + } if (tx.ancestors?.length) { flags |= TransactionFlags.cpfp_child; } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index f50274304..4a630f1e4 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -215,7 +215,7 @@ export const TransactionFlags = { replacement: 0b00000100_00000000_00000000n, // data op_return: 0b00000001_00000000_00000000_00000000n, - fake_multisig: 0b00000010_00000000_00000000_00000000n, + fake_pubkey: 0b00000010_00000000_00000000_00000000n, inscription: 0b00000100_00000000_00000000_00000000n, // heuristics coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, diff --git a/backend/src/utils/secp256k1.ts b/backend/src/utils/secp256k1.ts new file mode 100644 index 000000000..cc731f17d --- /dev/null +++ b/backend/src/utils/secp256k1.ts @@ -0,0 +1,74 @@ +function powMod(x: bigint, power: number, modulo: bigint): bigint { + for (let i = 0; i < power; i++) { + x = (x * x) % modulo; + } + return x; +} + +function sqrtMod(x: bigint, P: bigint): bigint { + const b2 = (x * x * x) % P; + const b3 = (b2 * b2 * x) % P; + const b6 = (powMod(b3, 3, P) * b3) % P; + const b9 = (powMod(b6, 3, P) * b3) % P; + const b11 = (powMod(b9, 2, P) * b2) % P; + const b22 = (powMod(b11, 11, P) * b11) % P; + const b44 = (powMod(b22, 22, P) * b22) % P; + const b88 = (powMod(b44, 44, P) * b44) % P; + const b176 = (powMod(b88, 88, P) * b88) % P; + const b220 = (powMod(b176, 44, P) * b44) % P; + const b223 = (powMod(b220, 3, P) * b3) % P; + const t1 = (powMod(b223, 23, P) * b22) % P; + const t2 = (powMod(t1, 6, P) * b2) % P; + const root = powMod(t2, 2, P); + return root; +} + +const curveP = BigInt(`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F`); + +/** + * This function tells whether the point given is a DER encoded point on the ECDSA curve. + * @param {string} pointHex The point as a hex string (*must not* include a '0x' prefix) + * @returns {boolean} true if the point is on the SECP256K1 curve + */ +export function isPoint(pointHex: string): boolean { + if ( + !( + // is uncompressed + ( + (pointHex.length === 130 && pointHex.startsWith('04')) || + // OR is compressed + (pointHex.length === 66 && + (pointHex.startsWith('02') || pointHex.startsWith('03'))) + ) + ) + ) { + return false; + } + + // Function modified slightly from noble-curves + + + // Now we know that pointHex is a 33 or 65 byte hex string. + const isCompressed = pointHex.length === 66; + + const x = BigInt(`0x${pointHex.slice(2, 66)}`); + if (x >= curveP) { + return false; + } + + if (!isCompressed) { + const y = BigInt(`0x${pointHex.slice(66, 130)}`); + if (y >= curveP) { + return false; + } + // Just check y^2 = x^3 + 7 (secp256k1 curve) + return (y * y) % curveP === (x * x * x + 7n) % curveP; + } else { + // Get unaltered y^2 (no mod p) + const ySquared = (x * x * x + 7n) % curveP; + // Try to sqrt it, it will round down if not perfect root + const y = sqrtMod(ySquared, curveP); + // If we square and it's equal, then it was a perfect root and valid point. + return (y * y) % curveP === ySquared; + } +} \ No newline at end of file diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts index 4a8cb6a15..0b652a192 100644 --- a/frontend/src/app/shared/filters.utils.ts +++ b/frontend/src/app/shared/filters.utils.ts @@ -29,7 +29,7 @@ export const TransactionFlags = { replacement: 0b00000100_00000000_00000000n, // data op_return: 0b00000001_00000000_00000000_00000000n, - fake_multisig: 0b00000010_00000000_00000000_00000000n, + fake_pubkey: 0b00000010_00000000_00000000_00000000n, inscription: 0b00000100_00000000_00000000_00000000n, // heuristics coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, @@ -64,7 +64,7 @@ export const TransactionFilters: { [key: string]: Filter } = { replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true }, /* data */ op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true }, - // fake_multisig: { key: 'fake_multisig', label: 'Fake multisig', flag: TransactionFlags.fake_multisig }, + fake_pubkey: { key: 'fake_pubkey', label: 'Fake pubkey', flag: TransactionFlags.fake_pubkey }, inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription, important: true }, /* heuristics */ coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin, important: true }, @@ -82,7 +82,7 @@ export const FilterGroups: { label: string, filters: Filter[]}[] = [ { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'multisig'] }, { label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] }, { label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement'] }, - { label: 'Data', filters: ['op_return', 'fake_multisig', 'inscription'] }, + { label: 'Data', filters: ['op_return', 'fake_pubkey', 'inscription'] }, { label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] }, { label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] }, ].map(group => ({ label: group.label, filters: group.filters.map(filter => TransactionFilters[filter] || null).filter(f => f != null) })); \ No newline at end of file From b0c02b16ff22058779a525ba9a02c0c34093a6ca Mon Sep 17 00:00:00 2001 From: natsee Date: Wed, 13 Dec 2023 19:22:46 +0100 Subject: [PATCH 23/33] Search result: do not offer Go to block --- .../app/components/search-form/search-form.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index a99dc084e..446d8be5a 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -174,7 +174,7 @@ export class SearchFormComponent implements OnInit { const addressPrefixSearchResults = result[0]; const lightningResults = result[1]; - const matchesBlockHeight = this.regexBlockheight.test(searchText); + const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight; const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date'; const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText); const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); @@ -217,7 +217,7 @@ export class SearchFormComponent implements OnInit { selectedResult(result: any): void { if (typeof result === 'string') { this.search(result); - } else if (typeof result === 'number') { + } else if (typeof result === 'number' && result <= this.stateService.latestBlockHeight) { this.navigate('/block/', result.toString()); } else if (result.alias) { this.navigate('/lightning/node/', result.public_key); @@ -232,8 +232,10 @@ export class SearchFormComponent implements OnInit { this.isSearching = true; if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) { this.navigate('/address/', searchText); - } else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) { + } else if (this.regexBlockhash.test(searchText)) { this.navigate('/block/', searchText); + } else if (this.regexBlockheight.test(searchText)) { + parseInt(searchText) <= this.stateService.latestBlockHeight ? this.navigate('/block/', searchText) : this.isSearching = false; } else if (this.regexTransaction.test(searchText)) { const matches = this.regexTransaction.exec(searchText); if (this.network === 'liquid' || this.network === 'liquidtestnet') { From 16b9ca6c4096e76fbd73f3feba6b1ed662c0ab68 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 14 Dec 2023 13:30:19 +0000 Subject: [PATCH 24/33] Fix CI unit test circular dependency --- backend/src/api/fee-api.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/api/fee-api.ts b/backend/src/api/fee-api.ts index 5260e959a..0cab5a295 100644 --- a/backend/src/api/fee-api.ts +++ b/backend/src/api/fee-api.ts @@ -1,8 +1,10 @@ import { MempoolBlock } from '../mempool.interfaces'; -import { Common } from './common'; +import config from '../config'; import mempool from './mempool'; import projectedBlocks from './mempool-blocks'; +const isLiquid = config.MEMPOOL.NETWORK === 'liquid' || config.MEMPOOL.NETWORK === 'liquidtestnet'; + interface RecommendedFees { fastestFee: number, halfHourFee: number, @@ -14,8 +16,8 @@ interface RecommendedFees { class FeeApi { constructor() { } - defaultFee = Common.isLiquid() ? 0.1 : 1; - minimumIncrement = Common.isLiquid() ? 0.1 : 1; + defaultFee = isLiquid ? 0.1 : 1; + minimumIncrement = isLiquid ? 0.1 : 1; public getRecommendedFee(): RecommendedFees { const pBlocks = projectedBlocks.getMempoolBlocks(); From 2e531413fa23b4ba93ff92877053bfb29b18aca3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 14 Dec 2023 18:09:04 +0000 Subject: [PATCH 25/33] Disable filter UI on mined blocks --- .../block-overview-graph/block-overview-graph.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html index 251b84a73..9f5e7cb47 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html @@ -13,6 +13,6 @@ [auditEnabled]="auditHighlighting" [blockConversion]="blockConversion" > - + From 100936e551815894c76c3576308a3393bcb53f32 Mon Sep 17 00:00:00 2001 From: orangesurf Date: Fri, 15 Dec 2023 10:04:04 +0000 Subject: [PATCH 26/33] Changes following feedback --- LICENSE | 2 +- frontend/src/app/components/about/about.component.html | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index f65965c7c..e5b707840 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ The Mempool Open Source Project® -Copyright (c) 2019-2023 The Mempool Space K.K. and other shadowy super-coders +Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 98365e91f..98dcb4c31 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -430,9 +430,7 @@
    From eae044cf665ef3f5f3288298dbeae146b2a6506c Mon Sep 17 00:00:00 2001 From: natsee Date: Fri, 15 Dec 2023 15:38:02 +0100 Subject: [PATCH 27/33] Show blockhash copy button in block component --- frontend/src/app/components/block/block.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 8bcf23ace..e908d5b24 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -42,7 +42,7 @@ Hash - ‎{{ block.id | shortenString : 13 }} + ‎{{ block.id | shortenString : 13 }} Timestamp From b3a68a0db2adde67a73e1fb79f407bffcf60bab4 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 15 Dec 2023 15:04:26 +0000 Subject: [PATCH 28/33] Smoother goggles color change transition --- .../block-overview-graph/block-overview-graph.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index 716ba540e..8a449a121 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -121,6 +121,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } else { this.scene.setColorFunction(this.overrideColors); } + this.start(); } ngOnDestroy(): void { From c2f52ac1f31a1674d7e6457c0c5bee9fd719a0d4 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 17 Dec 2023 09:57:15 +0000 Subject: [PATCH 29/33] Avoid repeated tx classification work --- backend/src/api/common.ts | 31 +++++++++++++++++++++---------- backend/src/mempool.interfaces.ts | 1 + 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 751bab5a3..4cd2b0873 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -222,7 +222,25 @@ export class Common { } static getTransactionFlags(tx: TransactionExtended): number { - let flags = 0n; + let flags = tx.flags ? BigInt(tx.flags) : 0n; + + // Update variable flags (CPFP, RBF) + if (tx.ancestors?.length) { + flags |= TransactionFlags.cpfp_child; + } + if (tx.descendants?.length) { + flags |= TransactionFlags.cpfp_parent; + } + if (rbfCache.getRbfTree(tx.txid)) { + flags |= TransactionFlags.replacement; + } + + // Already processed static flags, no need to do it again + if (tx.flags) { + return Number(flags); + } + + // Process static flags if (tx.version === 1) { flags |= TransactionFlags.v1; } else if (tx.version === 2) { @@ -306,15 +324,7 @@ export class Common { if (hasFakePubkey) { flags |= TransactionFlags.fake_pubkey; } - if (tx.ancestors?.length) { - flags |= TransactionFlags.cpfp_child; - } - if (tx.descendants?.length) { - flags |= TransactionFlags.cpfp_parent; - } - if (rbfCache.getRbfTree(tx.txid)) { - flags |= TransactionFlags.replacement; - } + // fast but bad heuristic to detect possible coinjoins // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse) const addressReuse = Object.values(reusedAddresses).reduce((acc, count) => Math.max(acc, count), 0) > 1; @@ -335,6 +345,7 @@ export class Common { static classifyTransaction(tx: TransactionExtended): TransactionClassified { const flags = this.getTransactionFlags(tx); + tx.flags = flags; return { ...this.stripTransaction(tx), flags, diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 4a630f1e4..d5dfcbf14 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -95,6 +95,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction { }; acceleration?: boolean; uid?: number; + flags?: number; } export interface MempoolTransactionExtended extends TransactionExtended { From a8fd2ac9dc2cb46cc8e7d99a767cffa824b89175 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 17 Dec 2023 10:45:26 +0000 Subject: [PATCH 30/33] Preserve tx replacement flag --- backend/src/api/common.ts | 2 +- backend/src/api/disk-cache.ts | 1 + backend/src/api/rbf-cache.ts | 13 +++++++++---- backend/src/api/redis-cache.ts | 1 + backend/src/mempool.interfaces.ts | 1 + 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 4cd2b0873..358a98c98 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -231,7 +231,7 @@ export class Common { if (tx.descendants?.length) { flags |= TransactionFlags.cpfp_parent; } - if (rbfCache.getRbfTree(tx.txid)) { + if (tx.replacement) { flags |= TransactionFlags.replacement; } diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index 093f07f0d..202f8f4cb 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -256,6 +256,7 @@ class DiskCache { txs: rbfData.rbf.txs.map(([txid, entry]) => ({ value: entry })), trees: rbfData.rbf.trees, expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })), + mempool: memPool.getMempool(), }); } } catch (e) { diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index ad1762485..a087abbe0 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -97,6 +97,8 @@ class RbfCache { return; } + newTxExtended.replacement = true; + const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction; const newTime = newTxExtended.firstSeen || (Date.now() / 1000); newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe); @@ -368,14 +370,14 @@ class RbfCache { }; } - public async load({ txs, trees, expiring }): Promise { + public async load({ txs, trees, expiring, mempool }): Promise { try { txs.forEach(txEntry => { this.txs.set(txEntry.value.txid, txEntry.value); }); this.staleCount = 0; for (const deflatedTree of trees) { - await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); } expiring.forEach(expiringEntry => { if (this.txs.has(expiringEntry.key)) { @@ -413,7 +415,7 @@ class RbfCache { return deflated; } - async importTree(root, txid, deflated, txs: Map, mined: boolean = false): Promise { + async importTree(mempool, root, txid, deflated, txs: Map, mined: boolean = false): Promise { const treeInfo = deflated[txid]; const replaces: RbfTree[] = []; @@ -426,9 +428,12 @@ class RbfCache { // recursively reconstruct child trees for (const childId of treeInfo.replaces) { - const replaced = await this.importTree(root, childId, deflated, txs, mined); + const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); if (replaced) { this.replacedBy.set(replaced.tx.txid, txid); + if (mempool[replaced.tx.txid]) { + mempool[replaced.tx.txid].replacement = true; + } replaces.push(replaced); if (replaced.mined) { mined = true; diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index 82ce34ad1..edfd2142b 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -222,6 +222,7 @@ class RedisCache { txs: rbfTxs, trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }), expiring: rbfExpirations, + mempool: memPool.getMempool(), }); } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index d5dfcbf14..c93372ded 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -94,6 +94,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction { vsize: number, }; acceleration?: boolean; + replacement?: boolean; uid?: number; flags?: number; } From bc89fd5b7c87ca8a1e5d05ce2d2ed8b87c5d7b95 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 15 Dec 2023 13:28:55 +0000 Subject: [PATCH 31/33] Goggles icon, beta tag and info tooltip --- .../block-filters.component.html | 8 +++-- .../block-filters.component.scss | 14 ++++++++ .../components/svg-icons/goggles.component.ts | 34 +++++++++++++++++++ frontend/src/app/shared/shared.module.ts | 6 +++- 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 frontend/src/app/shared/components/svg-icons/goggles.component.ts diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html index 90b66ddc3..876a72ad1 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.html +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -1,7 +1,11 @@
    + + beta + +
    -
    diff --git a/frontend/src/app/components/block-filters/block-filters.component.scss b/frontend/src/app/components/block-filters/block-filters.component.scss index 20b565293..6406a1d93 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.scss +++ b/frontend/src/app/components/block-filters/block-filters.component.scss @@ -20,7 +20,21 @@ margin-left: 0.5em; } + .info-badges { + display: flex; + flex-direction: row; + align-items: center; + float: right; + + &:hover, &:active { + text-decoration: none; + } + } + .menu-toggle { + width: 2em; + height: 2em; + padding: 0px 1px; opacity: 0; cursor: pointer; color: white; diff --git a/frontend/src/app/shared/components/svg-icons/goggles.component.ts b/frontend/src/app/shared/components/svg-icons/goggles.component.ts new file mode 100644 index 000000000..b045e6acb --- /dev/null +++ b/frontend/src/app/shared/components/svg-icons/goggles.component.ts @@ -0,0 +1,34 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-goggles-icon', + template: ` + + + + + + + `, +}) +export class GogglesIconComponent { + @Input() width: string = '100%'; + @Input() height: string = '100%'; +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 9bcfb932c..f772c3fd3 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -5,6 +5,7 @@ import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontaweso import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck } from '@fortawesome/free-solid-svg-icons'; +import { GogglesIconComponent } from './components/svg-icons/goggles.component'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '../components/menu/menu.component'; import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component'; @@ -200,6 +201,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerationsListComponent, AccelerationStatsComponent, PendingStatsComponent, + GogglesIconComponent, ], imports: [ CommonModule, @@ -322,7 +324,9 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir ClockFaceComponent, OnlyVsizeDirective, - OnlyWeightDirective + OnlyWeightDirective, + + GogglesIconComponent, ] }) export class SharedModule { From 8dbf879c37fc593531b2aa6d198fb611ebe6ba45 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 15 Dec 2023 13:29:29 +0000 Subject: [PATCH 32/33] Add Goggles FAQ entry --- .../src/app/docs/api-docs/api-docs-data.ts | 7 ++ .../app/docs/api-docs/api-docs.component.html | 89 +++++++++++++++++++ .../app/docs/api-docs/api-docs.component.scss | 41 +++++++++ 3 files changed, 137 insertions(+) diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index ce0b7eaef..97be0c2b1 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -8911,6 +8911,13 @@ export const faqData = [ fragment: "what-is-block-health", title: "What is block health?", }, + { + type: "endpoint", + category: "advanced", + showConditions: bitcoinNetworks, + fragment: "how-do-mempool-goggles-work", + title: "How do Mempool Goggles work?", + }, { type: "category", category: "self-hosting", diff --git a/frontend/src/app/docs/api-docs/api-docs.component.html b/frontend/src/app/docs/api-docs/api-docs.component.html index 49493d210..49b11ad7b 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.html +++ b/frontend/src/app/docs/api-docs/api-docs.component.html @@ -279,6 +279,95 @@

    Because of this feature's resource usage and availability requirements, it is only supported on official mempool.space instances.

    + +

    Mempool Goggles are a set of filters that can be applied to the mempool block visualizations to highlight different types of transactions.

    +

    There are currently 25 different Mempool Goggles filters, grouped into six categories:

    +
    +
    Features
    +
    +
    +
    RBF enabled
    +
    The transaction opts-in to BIP-125 replaceability.
    +
    RBF disabled
    +
    The transaction does not opt-in to BIP-125 replaceability.
    +
    Version 1
    +
    The default version for most transactions.
    +
    Version 2
    +
    Required for transactions which use OP_CHECKSEQUENCEVERIFY relative timelocks.
    +
    +
    + +
    Address Types
    +
    +
    +
    P2PK
    +
    Pay-to-public-key. A legacy output format most commonly found in old coinbase transactions.
    +
    Bare multisig
    +
    A legacy form of multisig, most commonly used for data embedding schemes (see also "Fake pubkey").
    +
    P2PKH
    +
    Pay-to-public-key-hash. A legacy address type that locks outputs to a public key.
    +
    P2SH
    +
    Pay-to-script-hash. A legacy address type that locks outputs to a redeem script.
    +
    P2WPKH
    +
    Pay-to-witness-public-key-hash. The SegWit version of P2PKH.
    +
    P2WSH
    +
    Pay-to-witness-script-hash. The SegWit version of P2SH.
    +
    Taproot
    +
    Addresses using the SegWit V1 format added in the Taproot upgrade.
    +
    +
    + +
    Behavior
    +
    +
    +
    Paid for by child
    +
    The transaction's effective fee rate has been increased by a higher rate CPFP child.
    +
    Pays for parent
    +
    The transaction bumps the effective fee rate of a lower rate CPFP ancestor.
    +
    Replacement
    +
    The transaction replaced a prior version via RBF.
    +
    +
    + +
    Data
    +
    + Different methods of embedding arbitrary data in a Bitcoin transaction. +
    +
    OP_RETURN
    +
    Fake pubkey
    +
    Data may be embedded in an invalid public key in a P2PK or Bare multisig output. This is a heuristic filter and can be prone to false positives and false negatives.
    +
    Inscription
    +
    Data is embedded in the witness script of a taproot input.
    +
    +
    + +
    Heuristics
    +
    + These filters match common types of transactions according to subjective criteria. +
    +
    Coinjoin
    +
    A type of collaborative privacy-improving transaction.
    +
    Consolidation
    +
    The transaction condenses many inputs into a few outputs.
    +
    Batch payment
    +
    The transaction sends coins from a few inputs to many outputs.
    +
    +
    + +
    Sighash Flags
    +
    + Different ways of signing inputs to Bitcoin transactions. Note that selecting multiple sighash filters will highlight transactions in which each sighash flag is used, but not necessarily in the same input. +
    +
    sighash_all
    +
    sighash_none
    +
    sighash_single
    +
    sighash_default
    +
    sighash_anyonecanpay
    +
    +
    +
    +
    + The official mempool.space website is operated by The Mempool Open Source Project. See more information on our About page. There are also many unofficial instances of this website operated by individual members of the Bitcoin community. diff --git a/frontend/src/app/docs/api-docs/api-docs.component.scss b/frontend/src/app/docs/api-docs/api-docs.component.scss index f90274046..8a4150262 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.scss +++ b/frontend/src/app/docs/api-docs/api-docs.component.scss @@ -389,3 +389,44 @@ h3 { margin-bottom: 4rem; } } + +/* styles for nested definition lists */ +dl { + margin: 0; + padding: 0; +} + +dt { + font-weight: bold; + color: #4a68b9; + padding: 5px 0; +} + +dd { + padding: 2px 0; + + & > dl { + padding-left: 1em; + border-left: 2px solid #4a68b9; + margin-left: 1em; + margin-top: 5px; + } + + & > dl > dt { + display: inline; + font-weight: normal; + color: #e83e8c; + font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New; + text-transform: uppercase; + + &:before { + content: ""; + display: block; + } + } + + & > dl > dd { + display: inline; + margin-left: 1em; + } +} From 78857102f8cdaea05411e5cdfc38d8a7aeb7588e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 17 Dec 2023 10:56:11 +0000 Subject: [PATCH 33/33] Move goggles icon inside generic svg-images component --- .../block-filters.component.html | 2 +- .../svg-images/svg-images.component.html | 8 +++++ .../components/svg-icons/goggles.component.ts | 34 ------------------- frontend/src/app/shared/shared.module.ts | 4 --- 4 files changed, 9 insertions(+), 39 deletions(-) delete mode 100644 frontend/src/app/shared/components/svg-icons/goggles.component.ts diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html index 876a72ad1..7b1c2f9e5 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.html +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -5,7 +5,7 @@
    diff --git a/frontend/src/app/components/svg-images/svg-images.component.html b/frontend/src/app/components/svg-images/svg-images.component.html index 5e8d7d29f..11dfe1d79 100644 --- a/frontend/src/app/components/svg-images/svg-images.component.html +++ b/frontend/src/app/components/svg-images/svg-images.component.html @@ -84,6 +84,14 @@ + + + + + + + + diff --git a/frontend/src/app/shared/components/svg-icons/goggles.component.ts b/frontend/src/app/shared/components/svg-icons/goggles.component.ts deleted file mode 100644 index b045e6acb..000000000 --- a/frontend/src/app/shared/components/svg-icons/goggles.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Component, Input } from '@angular/core'; - -@Component({ - selector: 'app-goggles-icon', - template: ` - - - - - - - `, -}) -export class GogglesIconComponent { - @Input() width: string = '100%'; - @Input() height: string = '100%'; -} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index f772c3fd3..f092e81bc 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -5,7 +5,6 @@ import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontaweso import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck } from '@fortawesome/free-solid-svg-icons'; -import { GogglesIconComponent } from './components/svg-icons/goggles.component'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '../components/menu/menu.component'; import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component'; @@ -201,7 +200,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerationsListComponent, AccelerationStatsComponent, PendingStatsComponent, - GogglesIconComponent, ], imports: [ CommonModule, @@ -325,8 +323,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir OnlyVsizeDirective, OnlyWeightDirective, - - GogglesIconComponent, ] }) export class SharedModule {